Quantcast
Channel: Planeta Código
Viewing all 2698 articles
Browse latest View live

Blog Bitix: Cómo documentar una API REST con Swagger implementada con Spring Boot

$
0
0

Una API REST no está obligada a publicar una definición de su API, sin embargo, para quien deba usar API es muy útil disponer de su documentación para usarla correctamente y descubrir de qué endpoints se compone, métodos HTTP, cuales son sus parámetros, el esquema de los cuerpos de la petición y de los resultados, los tipos de los datos y sus formatos, los códigos de retorno devueltos, las cabeceras y su autenticación. OpenAPI permite definir la interfaz de una aplicación de forma agnóstica de la tecnología y lenguaje en el que se implementa, por otro lado Swagger a partir de esa definición permite generar una interfaz HTML con su documentación. La librería Springdoc junto con Spring Boot permite generar tanto la especificación de la API como la documentación simplemente añadiendo una dependencia y varias anotaciones en la implementación de la API.

Java

Spring

Disponer de documentación es esencial para el desarrollo, también es el caso de tener que usar una API REST donde es necesario conocer que endpoints dispone la API, métodos HTTP, cuales son sus parámetros, el esquema de los cuerpos de la petición y de los resultados, los tipos de los datos y sus formatos, los códigos de retorno devueltos, las cabeceras y su autenticación.

GraphQL en sus especificaciones detallan además del protocolo define también una forma de exportar un esquema de la API y publicarlo junto con la misma que sirve como documentación. Una API REST que está basada más en convenciones y semántica del protocolo HTTP que en una especificación nada le obliga a proporcionar una especificación de la API. Aunque una API implemente HATEOAS e intente ser más autoexplicativa la documentación sigue siendo útil para explorar la API sin necesidad de realizar las peticiones.

No tener una especificación de la API es un inconveniente porque un cambio en la interfaz de la API puede provocar errores de compatibilidad, no tener su documentación para revisar la API dificulta su uso al implementar un cliente. No tener documentación es un inconveniente pero tener documentación no generada a partir del código fuente o de la especificación de la API también lo es porque la documentación corre el riesgo de no estar actualizada y sincronizada con la implementación en el código fuente. Además de quedar la documentación desactualizada respecto al código fuente requiere tiempo de mantenimiento que no se dedica a otras tareas.

Hay iniciativas y herramientas para suplir la carencia de las API REST de no obligar a proporcionar una especificación de la API REST y generar la documentación documentación a partir del código fuente. También es importante poder probar la API de forma sencilla, una de las formas más habituales de probar una API es que la documentación incluya el comando de la herramienta de línea de comandos curl por su sencillez ni requerimientos adicionales que tener el comando instalado en sistema para ejecutarlo.

Contenido del artículo

Documentación de un API con OpenAPI, Swagger y Springdoc

OpenAPI trata de proporcionar una especificación para definir esquemas de APIs agnósticas de la tecnología y la implementación de las APIs. Definida la interfaz de la API es posible crear un cliente o servidor que cumpla esa API. La definición de la API incluye sus endpoints, métodos HTTP, cuales son sus parámetros, el esquema de los cuerpos de la petición y de los resultados, los tipos de los datos y sus formatos, los códigos de retorno devueltos, las cabeceras y su autenticación.

Por otro lado las herramientas de Swagger permiten generar la documentación a partir de la especificación de la API y si se desea generar una implementación básica inicial de cliente y servidor para diferentes lenguajes de programación. La documentación de Swagger no solo incluye información sino que permite probar la API directamente desde la documentación u obtener el comando curl a ejecutar desde la línea de comandos.

En una aplicación que implementa una API REST con Spring Boot la librería Springpoc permite generar de forma automática la especificación de la API que implementa el código publicándose en un endpoint, esta librería también genera la documentación de Swagger de la API en otro endpoint.

Otra forma de obtener la especificación de la API es mediante el plugin para Gradle de springdoc o utilizar imagen de Docker de Swagger UI para crear un servidor que aloje la documentación. También es posible descargar la última versión de Swagger UI en el directorio dist, cambiar el archivo index.html y reemplazar la URL https://petstore.swagger.io/v2/swagger.json por la especificación de OpenAPI deseada.

1
2
./gradlew generateOpenApiDocs

gradlew-generateOpenApiDocs.sh

El documento en formato JSON incluye de la definición de la API, es un documento con el fin de ser utilizado por alguna herramienta como Swagger UI que en su caso genera la documentación en formato HTML.

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
{"openapi":"3.0.1","info":{    "title":"OpenAPI definition",    "version":"v0"},"servers":[    {      "url":"http://localhost:8080",      "description":"Generated server url"    }],"tags":[    {      "name":"message",      "description":"the message API"    }],"paths":{    "/message/":{      "get":{        "tags":[          "message"        ],        "summary":"Get all messages",        "description":"Returns all messages",        "operationId":"getAll",        "responses":{          "200":{            "description":"Successful operation",            "content":{              "application/json":{                "schema":{                  "$ref":"#/components/schemas/Message"                }              }            }          }        }      },      "put":{        "tags":[          "message"        ],        "summary":"Adds a message",        "description":"Add a message",        "operationId":"add",        "requestBody":{          "content":{            "application/json":{              "schema":{                "$ref":"#/components/schemas/Message"              }            }          },          "required":true        },        "responses":{          "200":{            "description":"Successful operation"          },          "400":{            "description":"Invalid data"          },          "409":{            "description":"Already exists"          }        }      }    },    "...":{    }},"components":{    "schemas":{      "Message":{        "type":"object",        "properties":{          "id":{            "type":"integer",            "format":"int64"          },          "text":{            "type":"string"          }        }      }    }}}
api-docs.json

La documentación en formato HTML de Swagger tiene el siguiente aspecto con la que además de obtener información sobre la API es posible ejecutar sus operaciones y obtener el comando curl para ejecutarlo desde la linea de comandos.

Documentación de Swagger UI de una API REST

Documentación de Swagger UI de una API RESTDocumentación de Swagger UI de una API REST

Documentación de Swagger UI de una API REST

Ejemplo de documentación REST con Spring Boot y Swagger

El siguiente ejemplo de Spring Boot implementa una pequeña API REST con un endpoint y varios métodos HTTP, uno para obtener un mensaje, otro para añadir un mensaje y otro para eliminar un mensaje. La API se define en un interfaz con las anotaciones tanto de Spring para REST como las anotaciones de Swagger para la definición de la API y documentación que al iniciar la aplicación permite generar la definición en formato OpenAPI.

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
packageio.github.picodotdev.blogbitix.javaswagger;...@Tag(name="message",description="the message API")@RequestMapping("/message")publicinterfaceRestApi{@Operation(summary="Get all messages",description="Returns all messages")@ApiResponses(value={@ApiResponse(responseCode="200",description="Successful operation",content=@Content(schema=@Schema(implementation=Message.class)))})@GetMapping(value="/",produces={"application/json"})ResponseEntity<List<Message>>getAll();@Operation(summary="Get a message by id",description="Return a message")@ApiResponses(value={@ApiResponse(responseCode="200",description="Successful operation",content=@Content(schema=@Schema(implementation=Message.class))),@ApiResponse(responseCode="400",description="Invalid id supplied"),@ApiResponse(responseCode="404",description="Message not found")})@GetMapping(value="/{id}",produces={"application/json"})ResponseEntity<Message>getById(@Parameter(description="Id of message to return",required=true)@PathVariable("id")Longid);@Operation(summary="Adds a message",description="Add a message")@ApiResponses(value={@ApiResponse(responseCode="200",description="Successful operation"),@ApiResponse(responseCode="400",description="Invalid data"),@ApiResponse(responseCode="409",description="Already exists")})@PutMapping(value="/",produces={"application/json"})ResponseEntity<Void>add(@Parameter(description="Id of message to return",required=true)@RequestBodyMessagemessage);@Operation(summary="Deletes a message by id",description="Delete a message")@ApiResponses(value={@ApiResponse(responseCode="200",description="Successful operation"),@ApiResponse(responseCode="400",description="Invalid id supplied"),@ApiResponse(responseCode="404",description="Message not found")})@DeleteMapping(value="/{id}",produces={"application/json"})ResponseEntity<Void>deleteBydId(@Parameter(description="Id of message to delete",required=true)@PathVariable("id")Longid);}
RestApi.java

La implementación de la API simplemente guarda en un mapa los mensajes, en caso de que detecte una condición de error lanza una excepción con el código de estado definido en la API para la condición, en caso de que la operación sea correcta se ejecuta su funcionalidad y se devuelve el código de estado 200 y los datos solicitados en su caso.

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
packageio.github.picodotdev.blogbitix.javaswagger;...@RestControllerpublicclassRestApiControllerimplementsRestApi{    privateMap<Long,Message>messages;    publicRestApiController(){        this.messages=newHashMap<>();        this.messages.put(1l,newMessage(1l,"Hello World!"));        this.messages.put(2l,newMessage(2l,"Welcome to Blog Bitix!"));    }    @Override    publicResponseEntity<List<Message>>getAll(){        List<Message>m=messages.entrySet().stream().map(e->e.getValue()).collect(Collectors.toList());        returnResponseEntity.ok(m);    }    @Override    publicResponseEntity<Message>getById(Longid){        if(!exists(id)){            thrownewResponseStatusException(HttpStatus.NOT_FOUND,"Message not found");        }        returnResponseEntity.ok(messages.get(id));    }    @Override    publicResponseEntity<Void>add(Messagemessage){        if(exists(message.getId())){            thrownewResponseStatusException(HttpStatus.CONFLICT,"Already exists");        }        if(message.isBlank()){            thrownewResponseStatusException(HttpStatus.BAD_REQUEST,"Invalid data");        }        messages.put(message.getId(),message);        returnResponseEntity.ok().build();    }    @Override    publicResponseEntity<Void>deleteBydId(Longid){        if(!exists(id)){            thrownewResponseStatusException(HttpStatus.NOT_FOUND,"Message not found");        }        messages.remove(id);        returnResponseEntity.ok().build();    }    privatebooleanexists(Longid){        returnmessages.containsKey(id);    }}
RestApiController.java

Con los siguientes comandos de curl es posible probar los diferentes métodos de la API.

1
2
#!/usr/bin/env bash
curl -v http://localhost:8080/message/
curl-get-all.sh
1
2
#!/usr/bin/env bash
curl -v http://localhost:8080/message/1
curl-get.sh
1
2
#!/usr/bin/env bash
curl -v -X PUT http://localhost:8080/message/ -H "Content-Type: application/json" --data '{"id": 3, "text": "Darkest Dungeon is a good game"}'
curl-put.sh
1
2
#!/usr/bin/env bash
curl -v -X PUT http://localhost:8080/message/ -H "Content-Type: application/json" --data '{"id": 1, "text": "Darkest Dungeon is a good game"}'
curl-put-conflict.sh
1
2
#!/usr/bin/env bash
curl -v -X DELETE http://localhost:8080/message/1
curl-delete.sh

Con la aplicación iniciada en en la URL http://localhost:8080/v3/api-docs por defecto se exporta especificación de la API en formato OpenAPI, en la URL http://localhost:8080/swagger-ui.html por defecto está la documentación de la API de Swagger generada por Springdoc. Con solo añadir las dependencias de Springdoc a la herramienta de construcción, en este caso Gradle, Spring Boot hace disponibles ambos endpoints.

 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
26
27
plugins{id'java'id'application'id'org.springframework.boot'version'2.5.2'id'com.github.johnrengelman.processes'version'0.5.0'id'org.springdoc.openapi-gradle-plugin'version'1.3.2'}application{group='io.github.picodotdev.blogbitix.javaswagger'version='0.0.1-SNAPSHOT'sourceCompatibility='11'mainClass='io.github.picodotdev.blogbitix.javaswagger.Main'}repositories{mavenCentral()}dependencies{implementationplatform('org.springframework.boot:spring-boot-dependencies:2.5.2')implementation'org.springframework.boot:spring-boot-starter'implementation'org.springframework.boot:spring-boot-starter-web'implementation'org.springdoc:springdoc-openapi-webmvc-core:1.5.9'implementation'org.springdoc:springdoc-openapi-ui:1.5.9'}
build.gradle
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Variable not found: ¡Microsoft MVP 2021-2022!

$
0
0
MVP Award

Es una gran alegría poder compartir con todos vosotros que Microsoft me ha reconocido por undécimo año consecutivo como Most Valuable Professional (MVP) en la comunidad de tecnologías para desarrolladores.

En esta ocasión, la notificación del nombramiento me pilló completamente desprevenido, apagando un fuego de los gordos provocado por un error en un servidor de producción, por lo que el fragor de la batalla no me dejó celebrar apropiadamente el momento, ni disfrutar la tensa espera que lo precede. Pero ya al llegar la calma, sí que he podido sentir de nuevo la emoción y orgullo que supone seguir perteneciendo un año más a este selecto grupo de locos por el software y la tecnología. ¡Once años ya, uau!

Muchas gracias a todos los que hacéis esto posible: a los que me estáis leyendo, porque sois los que día a día me dais motivos para continuar con este proyecto. No dudéis que sin vuestras visitas, comentarios y mensajes esto no tendría sentido. 

Muchas gracias también al equipo del programa MVP, por su incansable labor en beneficio de la comunidad y su inestimable ayuda a los miembros del programa. 

Y como no podía ser de otra forma, un agradecimiento infinito para mi mujer e hijas, por cederme el tiempo y espacio que necesito para desarrollar mi pasión.

Por último, me gustaría aprovechar la ocasión para felicitar a los MVP que renuevan este año y animarlos a continuar regalándonos tanto conocimiento y entusiasmo. Y también, por supuesto, una afectuosa bienvenida a los que acabáis de recibir vuestro primer reconocimiento; disfrutad el momento, porque no lo vais a olvidar jamás :)

¡Nos vemos!

Publicado en Variable not found.

Bitácora de Javier Gutiérrez Chamorro (Guti): Los archivos BGI y SVGA.BGI 4.00

$
0
0

Seguimos hablando de retroinformática, continuando con el Turbo Anti-Virus, Central Point Anti-Virus y Microsoft Anti-Virus, pero esta vez nuevamente centrado en el mundo de la programación. Si exceptuamos El curioso bug de QB64 (on error) creo que el último fue Uptime 2 para DOS de 2017. El Borland Graphics Interface (BGI) apareció en 1987 y …

Los archivos BGI y SVGA.BGI 4.00 Leer más »



Artículo publicado originalmente en Bitácora de Javier Gutiérrez Chamorro (Guti)

Blog Bitix: Análisis, guía y consejos del juego roguelike Darkest Dungeon

$
0
0

Si hay un juego en el que no hay una decisión perfecta o en el que hay que hacer sacrificios ese es Darkest Dungeon. Seguir avanzando en una mazmorra supone un riesgo, tocar un objeto sin la provisión adecuada puede tener efectos muy negativos, a veces para completar una misión hay que sacrificar un héroe, equiparse con un abalorio aparte de los efectos positivos la mayoría tiene efectos negativos. Darkest Dungeon es un juego difícil en el que en gran medida para completarlo hace falta conocerlo muy bien, sin una guía y consejos es muy difícil llegar al su final o requeriría mucho tiempo para aprender todos sus trucos, con algunos de ellos el éxito no está asegurado pero hay mayor probabilidad de conseguirlo. Está guía es introductoria no es exhaustiva para completarla incluye varios enlaces con mucha información para profundizar más.

Darkest Dungeon desarrollado por Red Hook Studios es uno de los mejores juegos en su categoría de rol y roguelike, destacando principalmente por sus mecánicas de juego que ofrecen gran cantidad de combinaciones y posibilidades. Es un juego difícil en el que los progresos se guardan de forma automática, de modo que las acciones realizadas no se pueden deshacer y si un héroe muere permanecerá muerto. En este juego el nivel de dificultad normal se puede considerar difícil, a lo largo del juego siempre se va al filo del desastre y en ocasiones hay tomar la decisión de que personaje es mejor que muera si es inevitable que uno lo haga, la mayoría de objetos que se encuentran y muchos curio tienen cosas negativas, aún con sus beneficios. Hay que exterminar el mal y es el precio a pagar.

Aún con su dificultad a medida que se juega se van aprendiendo sus mecánicas y funcionamiento que hace del juego más sencillo y con menos riesgos dentro de su constante dificultad que roza el estar siempre al al borde del desastre, a veces se sufren derrotas aunque hace la victoria es más satisfactoria. Su gráficos no son en tres dimensiones pero destacan por su arte y son suficientes para el juego, la música tétrica ambiental al juego en su mundo decadente y peligroso. El juego está traducido al español pero no está doblado, es posible colocar subtítulos para entender los diálogos.

Posee varias expansiones, The Crimson Court, The Colour of Madness y The Butcher’s Circus que añaden algunos héroes adicionales, nuevas localizaciones y algunas mecánicas de juego nuevas.

Está disponible tanto en PC como para consultas y en las principales tiendas digitales de juegos, en la tienda de la Epic Store estuvo gratis. Aunque es posible jugarlo en consolas, con ratón es mucho más sencillo jugarlo, con el mando el manejo de la interfaz se hace complicado por la limitación de botones y la falta de puntero.

Aún no me he pasado el juego y no he llegado al final pero ya he escrito este artículo porque este juego exige conocerlo para poder completarlo, al igual que en otros juegos de autoguardado las acciones son definitivas junto que además un error se penaliza llegando en el peor de los casos a suponer sin mucha dificultad la muerte de uno o varios héroes incluso de nivel alto. Parte de la dificultad del juego se basa en obtener conocimiento del él ya sea por experiencia, información leída o por consejos recibidos.

Un juego similar a Darkest Dungeon es el juego de rol For the King con algunas mecánicas similares de combates y exploración de mazmorras, sin embargo, Darkest Dungeon es mucho más complejo y difícil. A alguien que le guste uno de los dos juegos seguramente le guste el otro.

Pantalla inicialMenú

Pantalla inicial y menú del juego Darkest Dungeon

Anticipación del juego

Este artículo contiene información de estrategias para completar más fácilmente el juego, parte de la diversión de un juego es descubrir y superar los retos que se plantean por uno mismo. Sin embargo, algunos juegos son difíciles sin una pequeña ayuda que obliga a tener que dedicarles mucho más tiempo o a recomenzarlos.

En algunos juegos el argumento es una de las partes más importantes. El texto del artículo no contiene información acerca del argumento del juego, de la mitad o del final, ni hace ningún spoiler por lo que lo puedes leer sin riesgo de conocer alguna parte del argumento de forma anticipada. Sin embargo, algunos enlaces del artículo a otras páginas y vídeos sí pueden contener información del argumento de modo que recomiendo consultar solo las partes del juego una vez superadas.

Contenido del artículo

Héroes

Las misiones para explorar mazmorras se realizan en grupos de cuatro que hay que elegir en la preparación. Hay 16 clases de héroe distintas que reclutar entre los disponibles en los disponibles en la diligencia, cada clase de héroe tiene sus propias habilidades de combate y acampada y estadísticas base únicas entre las que se encuentran los puntos de vida los rasgos positivos y negativos, sus estadísticas de resistencias a varios efectos como aturdimiento y finalmente están las enfermedades que ha adquirido el héroe.

Algunos tienen  por su clase ciertos beneficios como la anticuaria que incluirla en un grupo permite aumentar acumular montones de dinero de 2500 en vez de 1750 y con beneficios a la obtención de objetos en las exploraciones, las anticuarias son especialmente buenas para obtener botín. Por otro lado el cruzado y la vestal tienen beneficios contra enemigos de tipo impío.

Las clases de los héroes son abominación, anticuaria, arbalestera, caza recompensas, cruzado, asaltatumbas, diabla, bandolero, maestro canino, bufón, leproso, hombre de armas, ocultista, doctor de plaga y vestal. En las expansiones del juego se añaden rompescudos, mosqueteros y flagelantes. Todos estos tipos de clases son únicos aunque algunos comparten habilidades con efectos comunes como el aturdir, causar hemorragia o infección, curar o habilidades de potenciación.

Independientemente de la clase de los héroes al embarcar en una misión hay que elegir un grupo de héroes equilibrado que cumplan los roles de tanque que será el que absorba daño, provocador de daño que será el que haga daño en grandes cantidades a los enemigos, curador que mantenga en buena salud al resto de héroes y soporte que proporcione potenciadores. Esta es la regla general algunos héroes tienen habilidades que permiten hacer la función de otro, en un grupo puede haber un héroe especializado en curar pero otro tener también alguna habilidad de curación para apoyar o un héroe proporcionar potenciadores pero también ser capaz de hacer un daño moderado.

A medida que los héroes realizan misiones ganan experiencia y suben de nivel, lo que mejora sus estadísticas y les permite equiparse con mejores armaduras y armas y un nivel mayor de habilidad. El nivel que tienen los clasifica en aprendiz, veterano y campeón, por otro lado las misiones también se clasifican en aprendiz, veterano y campeón. Los héroes se niegan a participar en misiones de menor nivel al que tienen. Los niveles 0, 1, 2 son de aprendiz, 3 y 4 es un veterano y 5 y 6 es un campeón. Pasar del nivel 5 al 6 no mejora las estadísticas base pero si las resistencias.

Cada héroe puede tener hasta 5 rasgos positivos que se ganan al subir de nivel  y 5 negativos que se adquieren también al subir de nivel o en algunos curio. Si un héroe ya tiene 5 rasgos y adquiere otro uno anterior es reemplazado, para no perder uno especialmente bueno es posible fijarlos en el sanatorio. Aunque los héroes tienen hasta 7 habilidades entre las que elegir en un mismo momento solo pueden estar activas 4 igual que las de acampada, estas hay que elegirlas por ser las mejores del héroe o por ser las más adecuadas según el grupo de héroes en una misión. Se pueden cambiar en el poblado y también entre los combates pero no en medio de un combate. Los héroes de la diligencia que se incorporan a los barracones con un nivel mayor que 0 las poseen todas a su mayor nivel a diferencia de los héroes que se incorporan de nivel 0 que no poseen todas.

AbominaciónAnticuariaArbalestera

Caza recompensasCruzadoAsaltatumbas

DiablaBandoleroMaestro canino

BufónLeprosoHombre de armas

OcultistaDoctor de plagaVestal

Héroes de Darkest Dungeon

Clases, habilidades más destacadas y funciones

Entre todos las diferentes clases de héroes en un grupo hay que elegir una combinación de ellos en el que sus habilidades se complementen o refuercen. Las funciones de los héroes son las siguientes:

  • Tanque: esta función de héroe es desempeñada por uno con una gran cantidad de puntos de vida y protección capaz de absorber mucho daño sin correr excesivo peligro. Aparte de absorber daño también puede tener la función de hacer daño y otras funciones de apoyo con algunas de sus habilidades como curar o proporcionar potenciadores. Las estadísticas principales de este héroes serán los puntos de vida y protección y en menor medida daño y evasión. Aunque un tanque también podría serlo por tener un alto valor de evasión.
  • Provocador de daño: esta función es la de causar gran cantidad de daño a los enemigos para eliminarlos de un solo golpe o quitar gran cantidades de vida a enemigos con gran cantidad de vida. Las estadísticas principales de los héroes para esta función son la de daño y probabilidad de crítico.
  • Curador: los enemigos también van a tener muchas oportunidades de realizar ataques y causar daño  los héroes, es imprescindible que haya un héroe que cuando sea necesario cure al resto para mantener los puntos de vida del resto en unos niveles que no les haga correr el riesgo de entrar a las puertas de la muerte en el que puedan morir. En el grupo de héroes es aconsejable que haya uno que cure puntos de vida y algún otro con la habilidad de bajar los puntos de estrés.
  • Potenciadores y mermas: otra función que puede desarrollar un héroe es la de soporte para proporcionar potenciadores al resto de héroes del grupo como un aumento de daño, esquiva o velocidad.

Todos las clases de héroe tienen tres habilidades de acampada comunes y cuatro únicas, que proporcionan potenciadores durante los cuatro siguientes combates o que curan esfuerzo, algunas habilidades tienen efectos negativos. Al realizar una acción con resultado de crítico los héroes obtienen un potenciador.

Cada tipo de héroe destaca y se diferencia en algunos aspectos del resto:

  • Abominación, ficha: es un héroe flexible ya que todas sus habilidades están a disposición con la transformación. Se puede curar la vida y esfuerzo.
  • Anticuaria: es el héroe con habilidades de ataque más débiles, su mayor utilidad es en su capacidad de aumentar el botín y monedas recuperados. En un combate su función es la de soporte.
  • Arbalestera, ficha: su posición es la 3 o 4, puede hacer grandes cantidades de daño a las últimas posiciones potenciado con los enemigos marcados y hacer las funciones de curación de soporte cuando no hay más enemigos a los que pueda atacar, su curación de batalla no cura mucho pero hace más efectivas las siguientes curaciones.
  • Caza recompensas, ficha: su posición es la 2 pero también puede estar en la 3, tiene la capacidad de mover y causa daño adicional a héroes marcados o aturdidos. No hace la función de tanque ya que no se potencia con protección.
  • Cruzado, ficha: su posición es la 1 o 2, hace la función de tanque potenciado su protección significativamente que dura durante toda la batalla. A los enemigos impíos les hace daño adicional. Tiene la capacidad de curar puntos de vida y esfuerzo dando soporte al curador principal.
  • Asaltatumbas, ficha: tiene habilidades que le dan movilidad pero no es adecuado junto a héroes que no la tengan, su evasión le permite usarse como tanque a base de evasión.
  • Diabla, ficha: su posición es la 1 o 2. Su habilidad Hachazo perverso le permite atacar a las dos primeras posiciones y con Cisne de hierro puede atacar a enemigos en la posición 4 lo que le permite atacar casi a cualquier posición del enemigo.
  • Bandolero, ficha: puede estar en cualquier posición causando grandes cantidades de daño con su notable probabilidad de crítico y aumento de daño, precisión y crítico con Disparo de seguimiento que dura durante todo el combate útil en los combates largos como los jefes.
  • Maestro canino, ficha: su posición es la 3 o 4 pudiendo llegar a todos los enemigos. Es especialmente adecuado para enemigos vulnerables a sangrado como las bestias pero inefectivo para no muertos que lo resisten. La habilidad Silvido quita mucha protección. Está equipado con dos piezas de galleta de premio que proporciona potenciadores al daño durante tres turnos.
  • Bufón, ficha: su posición ideal es la 3 donde puede usar Tonada inspiradora y usar sus ataques. El potenciador Balada de batalla que proporciona velocidad, precisión y crítico aplica a todo el grupo. Con la habilidad Apoteosis puede causar gran cantidad de daño para acabar con el último enemigo. Es más adecuado para misiones largas por su Tonada inspiradora que cura esfuerzo y potencia la resistencia al esfuerzo.
  • Leproso, ficha: puede ser considerado el mejor tanque del juego, tiene la capacidad de curarse puntos de vida significativamente y esfuerzo que lo hace autosuficiente. Solo es útil en las dos primeras posiciones, su otro defecto es que no tiene mucha precisión lo que hay que corregirlo con algún abalorio o potenciador.
  • Hombre de armas, ficha: su posición es la 1 o 2 donde puede usar todas sus habilidades, también es posible usarlo en la posición 4 donde potencia a los aliados en grupo mejorando precisión, velocidad y evasión, también puede reducir la evasión y velocidad los enemigos. Puede hacer de tanque absorbiendo daño.
  • Ocultista, ficha: su posición ideal es la 3, tiene habilidades para reducir a los enemigos más peligrosos reduciendo su daño y precisión además de marcarlo reduciendo su evasión. Tiene una poderosa habilidad de curación pero no totalmente fiable. Puede mover a enemigos a posiciones adelantadas donde no puedan usar sus habilidades más peligrosas o donde los compañeros de ataques cuerpo a cuerpo los eliminen.
  • Doctor de plaga, ficha: su posición es la 3, aunque sus habilidades tiene un daño base bajo causan efectos negativos al enemigo. Tiene una de las habilidades de movimiento más poderosas y la capacidad de aturdir a dos enemigos en la retaguardia. Tiene una habilidad de curación básica pero que elimina infecciones y sangrado.
  • Vestal, ficha: si posición ideal es la 3. Tiene dos poderosas habilidades de curación, una para un único objetivo y otra que cura menos pero afecta a todo el grupo. Con la habilidad Juicio puede atacar y curarse. Tiene habilidades que restauran el nivel de luz para conservar antorchas en misiones largas.

Por tipo

Las clases de los héroes que tienen como función principal en el grupo son:

  • Tanque: Cruzado, Leproso, Hombre de armas, Asaltatumbas (por evasión).
  • Provocador de daño: Caza recompensas, Asaltatumbas, Diabla, Bandolero, Abominación, Hombre de armas, Maestro canino, Arbalestera.
  • Curador (Puntos de vida, Esfuerzo): Ocultista (PV), Vestal (PV), Cruzado (PV, E), Bufón (E), Arbalestera (PV), Doctor de plaga (PV), Maestro canino(E).
  • Soporte, potenciadores y mermas: Bufón, Oultista, Doctor de plaga, Vestal, Hombre de armas, Maestro canino.

Otro aspecto en el que se pueden clasificar los héroes son por aquellos que tienen la capacidad de hacer daño a medida que pasa el tiempo ya sea por hemorragia o infección. O que tiene la capacidad de mover a enemigos de su posición a otra en la que no puedan utilizar su habilidad más poderosa o de moverse en caso de que los cambien de posición, también si pueden aturdir. La capacidad de hacer daño a enemigos en posiciones 3 y 4, también es importante pues en estas se suelen ubicar los enemigos más peligrosos.

  • Daño por tiempo (Infección, Hemorragia): Asaltatumbas (I), Diabla (H), Bufón (H), Doctor de plaga (H, I), Maestro canino (H), Anticuaria (I).
  • Capacidad de marcar: Caza recompensas, Ocultista, Maestro canino, Arbalestera.
  • Capacidad de aturdir: Doctor de plaga, Vestal, Abominación, Hombre de armas, Diabla.
  • Capacidad de moverse con ataque: Asaltatumbas, Cruzado, Diabla, Bandolero, Bufón, Abominación.
  • Capacidad de mover: Caza recompensas, Ocultista, Doctor de plaga, Abominación, Hombre de armas, Bandolero, Leproso.
  • Capacidad de hacer daño a enemigos en posiciones 3 y 4: Caza recompensas, Asaltatumbas, Diabla, Bufón, Ocultista, Vestal, Abominación, Hombre de armas, Maestro canino, Arbalestera.
  • Capacidad de curarse: Leproso, Abominación, Maestro canino.
  • Capacidad de mermar (Evasión, Daño, Protección, Crítico, Precisión, Velocidad): Ocultista (E, D, Pro), Arbalestera (Pre, C, E), Caza recompensas (Pro, D, V), Hombre de armas (E, V), Maestro canino (Pro), Leproso (D, V, Stealth).
  • Daño adicional (Marcado, Hemorragia, Infección, Aturdido): Arbalestera (M), Caza recompensas (M, Humano), Cruzado (Impío), Asaltatumbas (M, H, I), Bandolero (M), Maestro canino (M, Bestia), Ocultistas (Eldrich), Vestal (Impío).
  • Héroes religiosos: Leproso, Cruzado, Vestal.

La posición de los héroes también es importante, las habilidades y ataques tienen restricciones de posición.

  • Posición 1-2: Abominación (2), Cruzado (1), Diabla, Leproso (1), Hombre de armas.
  • Posición 2-3: Asaltatumbas, Bandolero, Caza recompensas (2).
  • Posición 3-4: Anticuaria, Arbalestera (4), Maestro canino, Hombre de armas, Vestal, Bufón (3), Ocultista (3), Doctor de plaga (3).

Poblado, edificios y recursos

El juego se desarrolla en dos zonas distintas, el poblado donde se prepara la siguiente misión con la exploración de mazmorras y combates.

En el poblado hay una serie de edificios que atienden a varias necesidades de los héroes como el reclutamiento con la caravana, la reducción de esfuerzo con la taberna y la abadía, el sanatorio para quitar rasgos y enfermedades, la armería y el gremio para mejorar habilidades de los héroes según alcanzan mayores niveles y la mejora de armaduras y arma, otras localizaciones son el cementerio donde ver los héroes caídos en combate, el carro nómada donde comprar nuevos abalorios y el superviviente donde adquirir nuevas habilidades de acampada.

Los edificios se pueden mejorar con reliquias que se obtienen durante las misiones como emblemas, bustos, cuadros y manuscritos. Estas mejoras permiten reducir los costes de usar los edificios, proporcionan acceso a entrenar habilidades, armas y armaduras de mayor nivel o más celdas en las salas de los edificios para tratar a los héroes.

Al finalizar una exploración de mazmorra los héroes habrán ganado esfuerzo, quizá alguna infección y si suben de nivel algún rasgo especialmente negativo. Al salir del combate es necesario tratar las afecciones especialmente negativas ganadas por héroes en la taberna, abadía y sanatorio de modo que no tengan penalizaciones en la siguiente misión.

FeudoCuidador

Feudo y cuidador

Mecánicas del juego

El juego no explica en gran detalle la mayor parte de sus mecánicas sino que estas se van aprendiendo al experimentarlas, esto provoca cometer errores, provocar la muerte de héroes y alargar el tiempo que se tarda en completar el juego. Algunas de las cosas que no se explican son las siguientes.

Durante una misión hay un nivel de luz que se va reduciendo a medida se recorre la mazmorra, las antorchas y algunas habilidades permiten aumentar el nivel de luz que afecta a mecánicas como el esfuerzo, probabilidad de ser sorprendidos y sorprender y patrulla. Es recomendable mantener siempre el nivel de luz en un nivel radiante superior a 75. El nivel de luz también afecta al botín obtenido siendo cuanto menos bajo mejor.

Las hemorragias e infecciones que causan daño al inicio de cada turno del héroe o enemigo se pueden acumular en caso de sufrir varias, el daño que causan que aparentemente no parece peligros con varias de estas acumuladas causan un daño significativo. También hay otros efectos de estado que son temporales y que afectan las estadísticas.

Los potenciadores y mermas tanto para héroes como enemigos también se acumulan e igual que las hemorragias e infecciones duran las rondas que indiquen.

En misiones medianas y largas existe la posibilidad de acampar después de la cual los héroes pueden ser sorprendidos por un grupo de enemigos.

Los héroes tienen un nivel y las misiones también tiene un nivel, los héroes de mayor nivel que la misión se niegan a participar en misiones de nivel bajo.

Los héroes pueden morir de dos formas, cuando están con cero puntos de vida en el estado de puertas a la muerte en este estado cada vez que sufran daño deberán superar un golpe mortal según su estadística de golpe mortal que por defecto es de un 67%. La otra forma de morir de un héroe es acumulando 200 puntos de esfuerzo momento en el cual es seguro que sufre un ataque al corazón y muere, si llega a 100 puntos adquiere un rasgo negativo que lastra al resto de héroes pudiendo causarles puntos de esfuerzo, realizando acciones aleatorias o efectos algunos leves pero otros muy nocivos.

El porcentaje de patrulla es de utilidad para evitar peligros al explorar mazmorras, descubrir trampas, sorprender a los enemigos que da la ventaja de tener el turno inicial adicional o evitar que el grupo de héroes sea sorprendido que da el turno adicional a los enemigos. También permite descubrir salas secretas con tesoros.

Localizaciones

En el juego sin expansiones hay 4 localizaciones, 5 contando la mazmorra oscura, en las expansiones se añaden una más. A medida que se completan misiones en las localizaciones estas suben de nivel  lo que causa que puedan aparecer enemigos más peligrosos y da acceso a que aparezca la misión de acabar con los jefes. El nivel de las localizaciones llega hasta al 7.

Cada localización tiene diferentes tipos enemigos. En las ruinas serán principalmente no muertos, nigromantes y cultistas de tipo impío con resistencia al sangrado pero con pocos puntos de vida pero vulnerables a la infección y menos peligrosos que los de otras localizaciones, en esta localización es posible encontrar gran cantidad de tesoros. En la foresta abundan los bandidos de tipo humano y otros seres que causan infecciones. En el laberinto es el lugar de las bestias y hombres cerdo vulnerables a la hemorragia. La cala es habitada por criaturas del mar y no muertos, abundan los enemigos con gran protección que causan infecciones y enfermedades y muchos curio proporcionan potenciadores o mermas o manipulan los rasgos.

La localización del Darkest Dungeon es el objetivo del juego, tiene algunos aspectos diferentes en el que no se genera de forma aleatoria ni se encuentra botín .

Localizaciones

Localizaciones

Estadísticas

Cada clase de héroe tiene sus propias habilidades de combate y acampada y estadísticas únicas entre las que se encuentran, puntos de vida, evasión, protección, velocidad, modificador de precisión, probabilidad de crítico y el rango de daño que realiza por ataque. También tienen varios rasgos positivos y negativos que afectan a las estadísticas de diferentes formas. Otro aspecto único de cada clase son las estadísticas de resistencias a varios efectos como aturdimiento, infección, enfermedad, golpe mortal, movimiento, hemorragia, merma y desarmado de trampas. Finalmente, están las enfermedades que ha adquirido el héroe si no ha superado una resistencia de enfermedad.

Las estadísticas bases y sus efectos son los siguientes:

  • Puntos de vida: son los puntos de vida del héroe. Al  iniciar una misión los héroes siempre empiezan con su nivel máximos de puntos de vida y se recuperan de forma completa al finalizar o abandonar la misión, al contrario que los puntos de esfuerzo que perduran y para reducirlos hay que utilizar la taberna o la abadía.
  • Evasión: es la esquiva del héroe, al medir el éxito de un ataque se tiene en cuenta junto con el porcentaje base de precisión y el modificador de precisión.
  • Protección: es un porcentaje que reduce el daño que sufre el héroe o enemigo al sufrir daño y calcula los puntos de vida que se pierde.
  • Velocidad: determina el momento en el que actúa el héroe teniendo en cuenta el resto de puntos de velocidad de otros héroes y enemigos. La línea de tiempo en la que actúan los héroes no aparece en pantalla lo que dificulta un poco en alguna ocasión utilizar alguna habilidad.
  • Modificador de precisión: es un modificador que se añade a la precisión base del héroe según su nivel de habilidad.
  • Probabilidad de crítico: es un porcentaje realizar ataque crítico que causa un mayor daño o cura un mayor puntos de vida de lo normal.

Las estadísticas de resistencias y sus efectos son las siguientes:

  • Aturdimiento: el aturdimiento causa la pérdida de la acción durante una ronda. La resistencia el porcentaje para resistirse a ser aturdido.
  • Infección: es un ataque que causa daño por tiempo al igual que la hemorragia. La resistencia es el porcentaje para resistirse a ser infectado.
  • Enfermedad: es el nivel de resistencia al sufrir una enfermedad que tiene la capacidad de modificar las estadísticas de los héroes. Las enfermedades se curan en el sanatorio.
  • Golpe mortal: es la resistencia a acabar muerto del héroe al sufrir un nuevo daño cuando está en las puertas de la muerte con cero puntos de vida. La estadística base es de un 67% de resistencia.
  • Movimiento: es la resistencia a ser cambiado de posición cuando sufre un ataque de movimiento.
  • Hemorragia: la hemorragia es otro ataque que causa daño por tiempo. La resistencia es el porcentaje para resistirse a sufrir hemorragia en un ataque que lo provoque.
  • Merma: la resistencia a evitar ataques que causen efectos de merma.
  • Desarmado de trampas: es el porcentaje de éxito al intentar desarmar una trampa.

El nivel de arma afecta al daño base, porcentaje de crítico base y velocidad base. El nivel de armadura afecta a los puntos de esquiva y puntos de vida. El nivel de habilidad afecta con porcentaje modificador al daño base, la precisión base y porcentaje de crítico base. Algunos abalorios modifican estas estadísticas, algunos aumentan unas estadísticas de forma positiva pero también pueden tener efectos negativos al mismo tiempo.

Una estadística que no aparece en la ficha del héroe es la virtud. Cuando un héroe alcanza los 100 puntos de esfuerzo tienen un porcentaje de en vez de obtener un rasgo negativo entrar en un estado virtuoso positivo y reducir su esfuerzo a 45. La probabilidad base de virtud es 25% que se puede modificar por rasgos y abalorios. Hay varios estados de virtud diferentes algunos permiten hacer más daño, curar esfuerzo a los compañeros, tener más protección y otros potenciadores a las estadísticas.

Ficha de héroe

Ficha de héroe

Rasgos y enfermedades

Los héroes al subir de nivel con la experiencia ganada después de completar una misión tienen la posibilidad de ganar rasgos, tanto positivos como negativos. También es posible que ganen algún rasgo al activar algún curio, en la mayoría de ocasiones serán negativos y algunas pocas positivos. Por otro lado, algunos enemigos con sus ataques provocan enfermedades si el héroe no supera la tirada de resistencia a la enfermedad.

Los rasgos positivos especialmente adecuados para el héroe es posible fijarlos en el sanatorio, los rasgos negativos es posible eliminarlos y las enfermedades es posible curarlas. Es posible fijar un rasgo positivo y eliminar un rasgo negativo al mismo tiempo.

Algunos rasgos negativos no son especialmente graves pero hay otros que hay que quitarlos si es posible porque afecta de forma muy negativa a las estadísticas del héroe o a una estadística importante. También hay rasgos positivos buenos, fijarlos evitar perderlos si en algún momento el héroe ya tiene 5 rasgos positivos y gana otro lo que provoca que alguno no fijado sea reemplazado.

Estos son los rasgos positivos más destacados junto con sus efectos e interesantes que los héroes poseen. Si son especialmente adecuados según su clase de héroe interesa fijarlos para que no sean reemplazados por otros.

  • Natural Eye: +5 a la precisión en ataques a distancia.
  • Natural Swing: +5 a la precisión en ataques a cuerpo a cuerpo.
  • Slugger: +10% de daño en ataques cuerpo a cuerpo.
  • Unerring: +10% de daño en ataques a distancia.
  • Eagle Eye: +3% a la posibilidad de crítico para ataques a distancia.
  • Precision Striker: +3% a la posibilidad de crítico para ataques cuerpo a cuerpo.
  • Deadly: +1% a la posibilidad de crítico.
  • Tough: +10% de vida máxima.
  • Evasive: +5 a evasión.
  • Quick Reflexes: +2 a la velocidad.
  • Hard Skinned: +10% a la protección.
  • Steady: +10% de resistencia esfuerzo.
  • Quickdraw: +4 a la velocidad durante la primera ronda de combate.
  • On Guard: +4 a la velocidad y +5 a evasión durante la primera ronda de combate.
  • Unyielding: +10% de resistencia a golpes mortales.

Según la función del héroe estos son los rasgos positivos más beneficiosos que tengan:

  • Un héroe de ataques a distancia: Eagle Eye, Deadly, Evasive. Estos héroes están maximizados para hacer daño con un poco de mayor velocidad.
  • Un héroe de ataques cuerpo a cuerpo: Slugger, Precision Striker, Hard Skinned. Un buen incremento en el daño y protección adicional.
  • Un héroe con la función de tanque: Slugger, Tough, Hard Skinned. Más protección, vida y algo más de daño en ataques cuerpo a cuerpo.
  • Un héroe para actuar primero especialmente en la primera ronda: Quick Reflexes, Quickdraw, On Guard. Esto proporciona puntos de velocidad en la primera ronda.
  • Un héroe con la función de curar: Tough, Quick Reflexes, Hard Skinned. Algo más de vida, de protección y algo de velocidad.

Los rasgos a evitar son los siguientes:

  • Los que reducen la velocidad: Nocturnal, Off Guard, Slowdraw, Slow Reflexes.
  • Los que reducen el daño: Light Sensitive, Scattering, Calm, Tuckered Out.
  • Los que fuerzan a los héroes a realizar acciones: Ablutomania, Bloodthirsty, Compulsive, Curious, Dacnomania, Dark Temptation, Demonomania, Dipsomania, Egomania, Guilty Conscience, Hagiomania, Hylomania, Kleptomaniac, Necromania, Paranormania, Plutomania, Sitiomania.
  • Los que causan robo de botín: Kleptomania.

Guía de rasgos.

Abalorios

Los abalorios son objetos que los héroes equipan, como máximo dos, que proporciona mejoras en algunas estadísticas sin embargo muchos de ellos al mismo tiempo también tiene un efecto negativo en alguna otra estadística. Son un elemento fundamental equipar a los personajes para potenciar sus virtudes o suplir sus defectos, tambié teniendo en cuenta el resto de personajes con los que va acompañado. Los abalorios se consiguen principalmente como una recompensa en el éxito en las misiones, los jefes también entregan abalorios importantes, también se pueden comprar en el carro nómada.

Estos son abalorios destacados para las funciones de algunos héroes, la mayoría aquellos que dan velocidad, evasión, los de daño, precisión y crítico para los que hacen daño, los de protección y resistencias para los tanques y algunos para suplir sus carencias como precisión para el leproso o potenciar su función como curaciones para los curadores.

HéroeAbaloriosEfecto
12
AbominaciónAncestor's Pen Feather Crystal más velocidad, evasión, crítico y daño
Berserk Charm Restraining Padlock más velocidad, daño, menos estrés causado en la transición a bestia
ArbalesteraAncestor's Musket Ball Wrathful Bandana más daño y crítico en ataques a distanicia
Bull's Eye Bandana Wrathful Bandana más precisión, critico y daño
Caza recompensasAncestor's Pen Hunter's Talon más crítico, precisión y daño en ataques de cuerpo a cuerpo
Hunter's Talon Wounding Helmet más crítico, precisión y daño cuerpo a cuerpo
CruzadoBerserk Charm Legendary Bracer más velocidad y daño
Tough Ring Berserk Charm más protección, vida y daño
Asalta tumbasRaider's Talisman Lucky Talisman más crítico, desarmado de trampas, patrulla, evasión y precisión
Feather Crystal Raider's Talisman más crítico, desarmado de trampas, patrulla, evasión y velocidad
DiablaAncestor's Pen Berserk Charm más daño, critico y velocidad
Ancestor's Pen Legendary Bracer más daño y crítico
BandoleroGunslinger's Buckle Berserk Charm Charm más daño, precisión en ataques a distancia y daño
Gunslinger's Buckle Ancestor's Musket Ball más daño y precisión en ataques a distancia
Maestro caninoFeather Crystal Camouflage Cloak más velocidad y evasión
Spiked Colar Feather Crystal más velocidad, evasión, daño y probabilidad de causar hemorragia
BufonFeather Crystal Camouflage Cloak más velocidad y evasión
Feather Crystal Ancestor's Coat más velocidad y evasión
LeprosoFocus Ring / Fortunate Armlet Tough Ring más precisión y crítico
Focus Ring Legendary Bracer más precisión, crítico y daño
Hombre de armasCamouflage Cloak Feather Crystal más velocidad y evasión
Guardian's Shield Feather Crystal más velocidad y evasión, en la posición 4 más protección, evasión y curaciones recibidas
OcultistaFeater Crystal Cleansing Crystal más velocidad, evasión y resistencias, menos probabilidad de causar hemorragia al curar
Camouflage Cloak Cleansing Crystal más velocidad, evasión y resistencias, menos probabilidad de causar hemorragia al curar
Doctor de plagaBlasphemous Vial Poisoned Herb más precisión y aturdir e infectar al enemigo
Blasphemous Vial Feater Crystal más precisión y probabilidad de aturdir e infectar al enemigo, evasión y velocidad
VestalSacred Scroll Tome of Holy Healing más precisión y aturdir e infectar al enemigo
Tough Ring Haste Chalice más vida máxima, protección y velocidad

Abalorios

Abalorios

Botines y curios

Los curio son objetos que se encuentran en los pasillos y en las salas de las mazmorras, dependiendo del curio activarlos permite conseguir una recompensa en reliquias o monedas pero algunos también tienen efectos negativos como ganar un rasgo negativo o una enfermedad. Los curio se pueden activar utilizando un objeto del equipo para evitar su probabilidad de efecto negativo y obtener siempre una recompensa positiva ya sea en reliquias, monedas, potenciadores o rasgo positivo. Cada curio tiene siempre los mismos efectos y probabilidad de resultado pero muchos de ellos tienen efectos negativos con lo que siempre hay que consultar antes el objeto adecuado con el que activarlo para evitar el riesgo de obtener un efecto negativo.

Las reliquias de la  familia son bustos, pinturas, hojas de diario y emblemas. Son los recursos con los que se mejoran los edificios. Es posible hacer conversiones entre cada uno de estos recursos.

BotínCurio

Botín y curio

Misiones y combates

Las misiones consisten en la exploración de una mazmorra, según la localización los enemigos serán unos u otros. Las misiones tienen un nivel (aprendiz, veterano, campeón), una duración (corta, mediana y larga), en función de los rasgos de los héroes un porcentaje de patrulla y un objetivo.

Los diferentes objetivos de las misiones son los siguientes:

  • Limpieza: ganar todos las batallas de la mazmorra.
  • Explorar: explorar el 90% de las salas.
  • Matar: acabar con un  jefe.
  • Activación: activar tres altares.
  • Recolectar: recolectar 3 reliquias.

Los héroes de un nivel se niegan a participar en misiones de un nivel inferior, los héroes con nivel inferior pueden participar en misiones de mayor nivel sin embargo es arriesgado ya que son débiles ante los enemigos de mayor nivel.

Los mapas de las mazmorras se generan de forma aleatoria siendo de mayor tamaño para las de mayor duración. En los pasillos y salas de las mazmorras es posible encontrar curio y trampas. Cuando se llega a una sala está vacía o está ocupada y después de eliminar a los enemigos se puede producir el efecto de patrulla que permite ver cómo es la mazmorra en las ubicaciones cercanas, con la patrulla también es posible encontrar salas ocultas, evitar ataques sorpresa de los enemigos y realizar ataques sorpresa con más facilidad al inicio de los combates a los enemigos.

Las misiones cortas no permiten acampada, las misiones medianas permiten acampar una vez y las largas dos veces para lo cual se añade un objeto en el inventario de leña, las acampadas se realizan en una sala de la mazmorra. Al acampar se emplean las habilidades de acampada, se proporcionan 12 puntos de acción a repartir entre las habilidades que se quieran utilizar, estas habilidades permiten curar puntos de vida, puntos de esfuerzo y proporcionar potenciadores que duran hasta 4 combates. Es recomendable acapar en la sala anterior antes de llegar a un jefe final para tener potenciadores que las habilidades causen mayor daño o llegar con unos niveles de esfuerzo y vida buenos. Después de acampar es posible que el grupo sea sorprendido por los enemigos y haya que realizar un combate.

Antes de empezar una misión está la fase de aprovisionamiento en la que se compran los objetos a llevar en el inventario, entre estos objetos están principalmente las antorchas y la comida y algunos otros objetos de utilidad como las vendas para curar hemorragias o antiveneno para curar infecciones, las palas para quitar obstáculos que si no se tiene causan gran daño y esfuerzo sin ellas, el agua bendita para proporcionar resistencias. Varios de estos objetos se pueden emplear para activar curio y obtener botín sin riesgos de efectos negativos. En función de la duración de la misión se equipan más objetos o menos, intentando que sean los mínimos imprescindibles pero siempre con un margen suficiente para completar la misión. Llevar muchos objetos en el inventario permite completar la misión con menos riesgo pero si se quiere recoger botín obliga a tirar objetos que no hubiera sido necesario aprovisionarse.

Después de los combates y con los curio se recoge un botín que ocupa lugar en el inventario, a veces hay que deshacerse de objetos para conseguir espacio para los objetos que se desean recoger. Al final de una misión hay que tratar de conseguir el mayor botín posible tanto en reliquias como en monedas para poder mejorar edificios, adquirir habilidades para los héroes o sanar a los héroes.

Los combates se desarrollan por turnos, cada héroe y enemigo tiene una acción, algunos enemigos dos. El orden de acción se determina por la estadística de velocidad, Un héroe realiza la acción usando una habilidad que se desee entre las equipadas. Para calcular el éxito del uso de una habilidad se utiliza la estadística de precisión y la estadística de evasión del objetivo. El daño se calcula con la estadística de daño y de protección del objetivo. Al realizar un ataque existe la posibilidad de realizar un crítico con el que se hace un daño o curación superior al normal, para ello se utiliza la estadística de probabilidad de crítico. Algunos ataques causan daño por tiempo mediante hemorragia o infección al inicio de la acción del héroe o enemigo, cuando un enemigo muere a causa de estos no deja su cadáver y los enemigos posteriores se desplazan a las primeras posiciones.

Las habilidades tiene unos requerimientos de posición en la que hay que estar para usarla y un requerimiento de posición del objetivo, por ejemplo algunas habilidades sólo son utilizables en la posición 3 y 4 contra objetivos en la posición 1 y 2.

Las provisiones recomendadas en función de la duración de la misión y la localización son las siguientes, las básicas son la comida y las antorchas el resto son de utilidad por el tipo de enemigos y el tipos de curio que se encuentran y son empleables entre ellos. Las provisiones son consumibles que tiene diferentes efectos, se pueden usar para conseguir sus efectos o al activar los curio para conseguir botín sin riesgo.

  • Comida: permite recuperar puntos de vida, al acampar los héroes consumen comida recuperando puntos de vida y en caso de consumir muchas raciones también esfuerzo.
  • Antorchas: aumenta el nivel de luz que desciende a medida que se exploran las mazmorras.
  • Palas: permite quitar obstáculos en los pasillos, si no se dispone de una pala para quitar un obstáculo los héroes sufren grandes cantidades de daño.
  • Vendas: cura las hemorragias.
  • Antiveneno: cura las infecciones.
  • Hierbas medicinales: quitas las mermas que posea un héroe.
  • Llaves esqueleto: para activar curio y recoger botín sin riesgo.
  • Agua bendita: aumenta las resistencias.
  • Láudano: quita el efecto de horror.

AprovisionamientoAprovisionamientoAcampada

Misión, aprovisionamiento y acampada

Combate

Combate

Jefes

Entre las misiones y objetivos de juego está unas misiones especiales que implican  matar a un jefe. Son enemigos especiales más resistentes, poderosos y cada uno con sus propios comportamientos. Cada jefe es distinto, requiere de su propia estrategia y grupo de héroes para acabar con él sin que mate a ningún héroe.

Los jefes aparecen varias veces a medida que se progresa en el nivel de las localizaciones. Al matarlos estos entregan abalorios especiales algunos de los cuales son poderosos y útiles para conseguirlos antes de entrar en la mazmorra oscura.

Los jefes y sus estrategias de batallas son las siguientes:

Estos son algunos jefes que hay que aniquiar para completar el juego.

BrujaCañonNigromante

Bruja, Cañon y Nigromante

ProfetaSirena

Profeta y Sirena

Estrategia para completar el juego

Este juego para completarlo requiere conocer en profundidad muchos de sus aspectos. Es difícil y a poco error que se cometa o mala suerte que se tenga un héroe puede morir. Para completarlo más fácilmente los siguiente consejos de estrategia son de mucha ayuda, que héroes son los más adecuados para cada localización, qué edificios mejorar primero, que cantidades de provisiones llevar para cada misión y localización, estrategias para enfrentarse a los jefes y a la mazmorra oscura.

Auqí solo doy unos pocos consejos generales, en intenet hay guías mucho más completas y detalladas.

Aviso de la dificultad del juego

Aviso de la dificultad del juego

Estrategia de grupos de héroes

De enviar a una misión un grupo de héroes equilibrado para la ubicación dependen en gran medida el éxito de la misión, junto con las habilidades adecuadas y las provisiones necesarias. Un buen grupo de héroes es aquel que tiene miembros para desempeñar las funciones principales de un grupo, tanque, realizador de daño, curador y soporte. Este esquema admite algunas variaciones dependiendo el objetivo. Enviar a una misión a un grupo de héroes inadecuado o desequilibrado supone sin dificultad la muerte de alguno de ellos.

En las ruinas hay abundancia de enemigos impíos inmunes a la hemorragia, una buena cantidad de PV y ataques de daño y esfuerzo. Una opción es aturdir a los de las primeras posiciones y dañar a los de la trasera junto con infecciones, también es posible poner a los enemigos en la posición trasera en la delantera para atacarlos con ataques de cuerpo a cuerpo. El Cruzado y la Vestal tienen bonificadores de daño impíos. La Anticuaria puede incluirse si el objetivo es conseguir botín.

  • Héroes recomendados para las ruinas: Cruzado, Hombre de armas, Leproso, Doctor de plaga, Vestal, Abominación, Asaltatumbas, Bufón, Anticuaria, Caza recompensas.

La foresta es el área más compleja. Algunos enemigos son rápidos, otros fuertes, a veces se prioriza la línea delantera y a veces la línea trasera, no hay un solo tipo de enemigos como impíos en las ruinas y eldrich en la cueva. El daño recibido proviene de infecciones, hemorragia o críticos de esfuerzo, también de movimiento. Los enemigos son vulnerables a la hemorragia y la infeción mientras que la infección es inefectiva, tiene la habilidad de marcar que hace la habilidad de proteger o quitar la marca útil.

  • Héroes recomendados para la foresta: Ocultista, Bufón, Abominación, Hombre de armas, Bandolero, Leproso, Maestro canino, Caza recompensas, Diabla, Arbalestera.

Los enemigos del laberinto son humanos y bestias, rápidos con buena evasión, con una combinación de los tres tipos de daño, físico, por hemorragia e infección y resistencias a infecciones. A diferencia de otras áreas el esfuerzo es más manejable. El aturdimiento para las posiciones traseras y las hemorragias funcionan muy bien. Las hierbas medicinales sirven para eliminar las mermas que infligen y activar curio.

  • Héroes recomendados para el laberinto: Hombre de armas, Diabla, Caza recompensas, Maestro canino, Bandolero, Asaltatumbas, Ocultista, Doctor de plaga.

Algunos enemigos de la cala causan daño, otros esfuerzo, otros apilan daño de hemorragia rápidamente y en grandes cantidades, también tiene protección con habilidades de proteger. Los enemigos son vulnerables al aturdimiento e infecciones. Se necesita velocidad, aturdimiento, daño y cura, si se añade daño contra eldritch e impíos las misiones se pueden completar sin recivir grandes cantidades de daño.

  • Héroes recomendados para la cala: Cruzado, Hombre de armas, Doctor de plaga, Vestal, Ocultista, Abominación, Diabla, Asaltatumbas.

Más análisis y consejos de grupos:

Estrategia en misiones

Al organizar una misión es necesario fijarse en cuál es el objetivo de la misión, en qué localización de desarrollar para elegir los héroes más efectivos, su duración para equipar el número de objetos en el inventario adecuados, el nivel de la misión, el porcentaje de patrulla del grupo y si algún personaje tiene fobia a esa localización o por el contrario está especiaizado en ella con ventajas.

El primer paso de una misión es construir el grupo de héroes tratando de que haya uno que realice la función de tanque, provocador de daño, curador y soporte. Algunos héroes pueden hacer la función de otro en caso de necesidad aunque no sea su función principal. Después de una misión es recomendable hacer descansar a los héroes en caso de que hayan regresado con mucho esfuerzo y curarles los rasgos negativos y enfermedades para que en siguientes misiones no tengan penalizaciones. Un héroe con mucho esfuerzo no debería participar en un nueva misión ya que corre el riesgo de ganar un rasgo negativo si llega a 100 de esfuerzo o de morir si llega a 200 en una mala sucesión de ataques. Alguno de los héroes debería tener al menos más de 80% en la desactivación de trampas, al desactivarlas correctamente permite evitar su efecto negativo y al mismo tiempo curar un poco de esfuerzo en el héroe que la desactiva.

El segundo paso es hacer que el resto de héroes que no participan en la misión aprovechen para que recuperen esfuerzo en la taberna o abadía, o curarles rasgos negativos y enfermedades en caso de que tengan. Los héroes de nivel bajo que tengan muchas taras no merece la pena invertir monedas en ellos, es mejor contratar un nuevo héroe que esté menos mermado.

El tercer paso es organizar el aprovisionamiento en función de la localización, nivel y duración de la misión. Con estas recomendaciones generales en función de la duración.

LocalizaciónRuinasForestaLaberintoCala
LocalizaciónCMLCMLCMLCML
Comida12 16-18 18-24
Antorchas8-9 14-16 16-18
Palas1-2 2 2-3 34-55-722-3411-22
Vendas1 2 2-3 22-341-222-32-33-45-6
Antiveneno0 0 0 22-34-500-11000
Hierbas medicinales1 2 2 11-22-32-345-611-22-3
Llaves esqueleto1 2 3-4 12222-33-4123-4
Agua bendita2 3 4 122-3233-400-11-2

Guías de localizaciones.

Al finalizar una misión hay que conseguir el mayor botín posible, si no hay más hueco en el inventario y se está cerca de finalizar la misión es posible deshacerse de objetos de menor valor y que ya es seguro no son necesarios o se puede completar la misión sin ellos como llaves, vendas, agua bendita, palas e incluso comida y antorchas.

Al finalizar la misión conviene curar los rasgos negativos que hayan conseguido por subir de nivel y las enfermedades para que en la siguiente misión en la que participen no tengan penalizaciones.

Estrategia en los combates

Los enemigos más peligrosos suelen estar en las posiciones 3 y 4 con lo que es recomendable eliminar estos primero o anularlos con aturdimiento o movimiento para cambiarles de posición de modo que no puedan utilizar sus habilidades más peligrosas. Para ello es necesario que en el grupo haya algún héroe capaz de hacer daño en las posiciones 3 y 4. Una vez eliminados los enemigos más peligrosos se acaba con el resto. Cuando queden un solo enemigo es posible aprovechar para utilizar habilidades de curación de puntos de vida o esfuerzo para afrontar el siguiente combate con buenos niveles, si el enemigo causa menos daño del que los héroes se curan se puede pasar algún turno sin matarlo empleándolo para curar, siempre teniendo en cuenta que no es posible pasar de esta forma muchos turnos ya que existe la posibilidad de que el enemigo llame a refuerzos.

En el caso de las misiones que involucran jefes hay que conocer que estos están siempre en la sala más profunda de la mazmorra, con que que si se desea ir directamente a por su ubicación aunque oculta es conocida. Si es una misión mediana conviene aprovechar a descansar en la sala anterior para aplicar potenciadores a los héroes y realizar curas. Hay que tratar que los combates contra los jefes duren el menor número de turnos, a la larga ellos tiene la de vencer por las grandes cantidades de daño que hacen.

Estrategia en edificios

Al inicio de la partida el edificio más importante y el primero a realizar las primeras mejoras es la diligencia y el barracón para tener disponibles más héroes entre los que elegir y en un futuro tener la posibilidad de incorporar héroes de mayor nivel al cero. Los héroes incorporados con un nivel mayor que cero tiene la ventaja de que vienen con todas las habilidades adquiridas a su mayor nivel según el nivel del héroe. Mejorando los barracones es posible tener un grupo de hasta 28 héroes entre los que elegir y mejorando la diligencia permite en ocasiones tener disponibles héroes de hasta nivel 3.

Los siguiente edificios a mejorar son la armería y gremio que permite mejorar los niveles de las habilidades de los héroes y el arma y armadura, algunas de las mejoras hacen que cuesten menos monedas las mejoras de los héroes .

El siguiente edificio es el sanatorio para quitar rasgos negativos y enfermedades. Los siguientes serán la taberna y la abadía cuando los héroes vuelvan con cantidades importantes de esfuerzo. Los últimos edificios del poblado a mejorar son la superviviente que únicamente reduce el coste de adquirir las habilidades de acampada y el carromato nómada que permite tener disponibles más abalorios para comprar cada semana y comprar algún abalorio especialmente interesante.

El orden de preferencia de mejora de los edificios es el siguiente: Diligencia, Gremio, Herrería, Sanitario, Superviviente, Abadía, Taberna. Los retratos es el recurso más raro de las reliquias, es preferible guardar estos que otros en caso de tener que desechar el botín.

Los edificios se van desarrollando de forma progresiva una opción es aumentar la diligencia a 4 héroes por semana y 16 de capacidad en los barracones. El gremio y la herrería a nivel 2 es suficiente para llegar subir a nivel 3 a los héroes para hacer asequibles las mazmorras. Luego reclutas experimentados a nivel 1, es una buena mejora ya que proporciona héroes ya con las habilidades mejoradas y todas adquiridas, con una anticuaria se tarda menos en alcanzar las mejoras por sus beneficios en el botín. Barracones a 24. Gremio y herrería a nivel 3 permite reclutar héroes con nivel 2. El Sanatorio sabiendo que curios son peligrosos y cómo obtener su botín usando la provisión adecuada no es necesario, salvo alguna de los rasgos negativos a evitar no es necesario quitarlos hasta a partir del nivel 4. Para las mazmorras de nivel campeón es necesario el Gremio y la Herrería a nivel 5.

Estrategia para el Darkest Dungeon

Entrar al Darkest Dungeon para acabar con el mal es uno de los objetivos principales del juego, estas mazmorras son especialmente difíciles y hay que entrar con los héroes de nivel 6 con el máximo de provisiones. A diferencia de las mazmorras de otras localizaciones no son aleatorias, no hay obstáculos ni cofres que abrir con llaves, no hay probabilidad de recibir un ataque sorpresa después de descansar ni hay botín que recoger. Hay cuatro mazmorras a completar en esta localización. Los enemigos causan gran cantidad de esfuerzo por lo que no es aconsejable llevar los abalorios más poderosas que tiene penalizaciones al esfuerzo.

Más información y wiki

Darkest Dungeon aún con sus mecánicas aparentemente sencillas es un juego bastante complejo y con muchas combinaciones posibles con una dificultad elevada. El juego no explica muchos de sus conceptos con lo que aprender cómo funcionan requiere experiencia, en este aprendizaje a veces se comenten errores que en el peor de los casos cuesta la vida a alguno o varios de los héroes.

Ha varias enciclopedias que recogen mucha información del juego desde las localizaciones, héroes y sus estadísticas, armas, armaduras, habilidades, abalorios, rasgos y enfermedades, enemigos y curio. Dos de estas enciclopedias más completas son la siguientes:

Otras fuentes de información son ver jugar a otros jugadores en Twitch o algún video de YouTube en el que se aprende y explican algunas buenas combinaciones para facilitar el completar el juego. En Reddit también se encuentran hilos sobre temas específicos.

Banda sonora original

La banda sonora ambienta el juego de forma tétrica y decadente.

Variable not found: Enlaces interesantes 451

$
0
0
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET / Blazor

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

    Machine learning / IA / Bots

    Web / HTML / CSS / Javascript

    Visual Studio / Complementos / Herramientas

    Xamarin / Maui

    Otros

    Publicado en Variable not found.

    Variable not found: ¿Se pueden crear componentes Blazor que no hereden de ComponentBase o alguno de sus descendientes?

    $
    0
    0
    Blazor

    Como sabéis, a no ser que se especifique lo contrario mediante la directiva @inherits, los componentes Blazor heredan de Microsoft.AspNetCore.Components.ComponentBase. Esta clase abstracta proporcionada por el framework hace que podamos centrarnos en escribir nuestro código, al encargarse de gestionar aspectos básicos de los componentes como su creación, inicialización, renderizado, el ciclo de vida u otros aspectos de infraestructura.

    Pero aunque heredar de ComponentBase es la inmensa mayoría de las veces la mejor opción, no tiene por qué ser así necesariamente. Blazor reconoce como componente cualquier clase que implemente IComponent, una pieza mucho más pequeña.

    La interfaz IComponent

    Todos los componentes Blazor implementan IComponent, definido en el espacio de nombres Microsoft.AspNetCore.Components de la siguiente forma:

    public interface IComponent
    {
    void Attach(RenderHandle renderHandle);
    Task SetParametersAsync(ParameterView parameters);
    }

    Como podéis observar, la interfaz se reduce a dos métodos:

    • Attach(), que es invocado cuando el elemento es introducido en el DOM.
    • SetParametersAsync(), invocado tras el anterior, al cargar los parámetros por primera vez, y cada vez que se modifique el valor de éstos.

    Dicho esto, veamos el ejemplo más sencillo posible que usa esta interfaz. El componente HelloWorld mostrará el clásico saludo cuando sea introducido en el DOM de una página:

    <h1><HelloWorld /></h1>

    Su código fuente:

    public class HelloWorld : IComponent
    {
    public void Attach(RenderHandle renderHandle)
    {
    renderHandle.Render(builder => builder.AddContent(1, "Hello, world!"));
    }

    public Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask;
    }

    El método Render() del RenderHandle recibe como parámetro un RenderFragment que, como ya hemos visto, es un delegado que permite "escribir" el contenido que será insertado en el DOM al renderizar el componente. En este caso, hemos escrito el delegado en forma de lambda que recibe el RenderTreeBuilder para generar el contenido.

    De esta forma, al llamar a Render() en el método Attach(), estamos forzando a que el contenido sea renderizado justo en el momento en que el componente es insertado en el DOM.

    Vamos a complicar algo más el componente añadiéndole un parámetro que permita indicar un nombre al que saludar. Como podemos comprobar, sigue siendo bastante sencillo:

    public class HelloWorld : IComponent
    {
    private RenderHandle _renderHandle;

    public void Attach(RenderHandle renderHandle)
    {
    _renderHandle = renderHandle;
    }

    public Task SetParametersAsync(ParameterView parameters)
    {
    var name = parameters.GetValueOrDefault<string>("Name") ?? "World";
    _renderHandle.Render(builder => builder.AddContent(1, $"Hello, {name}!"));
    return Task.CompletedTask;
    }
    }

    Fijaos que en este caso hemos desplazado el renderizado al método SetParametersAsync(), que es donde se establecen los parámetros una vez ha sido creado el componente. Para ello hemos tenido que almacenar localmente el RenderHandle suministrado a Attach().

    Este componente podría usarse ahora de esta manera:

    <h1><HelloWorld /></h1> <!-- Muestra Hello, World! -->
    <h1><HelloWorld Name="Jon" /></h1> <!-- Muestra Hello, Jon! -->

    Estos serían ejemplos de los componentes más simples que podemos implementar, lo más pegado al metal que Blazor nos permite. Sobre estos mimbres está construido ComponentBase y todo lo que, sobre él, vayamos montando en nuestras aplicaciones.

    ¿Y es esto útil en la práctica? Pues no mucho más allá de satisfacer nuestra curiosidad y saber cómo funcionan las cosas por dentro 😉. Salvando algunas excepciones de componentes no visuales que no requieran ciclo de vida ni nada de lo que ComponentBase nos facilita, no puedo imaginar casos en los que esta fórmula para construir componentes pueda resultar útil en nuestro día a día.

    Pero bueno, ¿y lo que aprendemos por el camino?

    Publicado en Variable not found.

    Header Files: Argumentos expresivos (parte 2)

    $
    0
    0

    Se suele decir que lo más difícil de la programación no es escribir código, es leerlo. Y los que hemos trabajado con bases de código de varios lustros de edad lo sabemos muy bien: funciones que tenemos que leer durante un par de horas para saber qué hacen, cómo lo hacen, sus precondiciones, sus casos borde, sus efectos colaterales. Muchas veces tenemos que pasar por largas sesiones de depuración paso a paso y refactorización para descifrar ese método que otro (¿nosotros?) escribió hace años (¿meses? ¿semanas?). Para más información sobre el trabajo con código legado recomiendo esta lectura.

    La mejor forma de resolver este problema es evitarlo: escribir código que no sólo ha de ser ejectuado por un ordenador sino que ha de ser leído por un ser humano. La expresividad del código es un tema que desde hace unos años me viene apasionando más y más, ya que muchas veces con muy poco esfuerzo es posible mejorar la calidad, legibilidad y mantenibilidad del código drásticamente. Y casi siempre sin añadir overhead a nuestro proyecto.

    En esta entrega extenderemos lo expuesto a comienzos de año a más tipos de datos de una forma muy sencilla.

    Argumentos booleanos expresivos

    Para refrescar, comentábamos que podíamos crear un tipo booleano con un propósito específico, que no fuera convertible implícitamente, y por lo tanto de forma oculta a nuestros ojos:

    structTrueFalse{constboolvalue;explicitTrueFalse(boolvalue):value{value}{}explicitTrueFalse(intvalue)=delete;explicitTrueFalse(constvoid*value)=delete;explicitTrueFalse(doublevalue)=delete;operatorbool()const{returnvalue;}};#define DEF_TRUE_FALSE(name) struct name : TrueFalse { using TrueFalse::TrueFalse; }
    DEF_TRUE_FALSE(ReadOnly);

    Generalización

    Basándonos en esta solución es posible generalizar parte de la clase para soportar cualquier tipo de dato (aprovecharemos de extender algunas funcionalidades y de mejorar el código)

    template<typenameT>classExplicitValue{Tvalue;public:explicitExplicitValue(Tvalue)noexcept:value{value}{}ExplicitValue(constExplicitValue&other)noexcept:value{other.value}{}ExplicitValue(ExplicitValue&&other)noexcept:value{std::move(other.value)}{}ExplicitValue<T>&operator=(constExplicitValue&other){value=other.value;return*this;}ExplicitValue<T>&operator=(ExplicitValue&&other){value=std::move(other.value);return*this;}operatorT()const{returnvalue;}};#define DEF_EXPLICIT_VALUE(name, Type) class name : public ExplicitValue<Type> { using ExplicitValue::ExplicitValue; }
    #define DEF_TRUE_FALSE(name) DEF_EXPLICIT_VALUE(name, bool)
    

    Por último, podemos extender esta funcionalidad aún más definiendo un literal de usuario para construir el tipo de dato. Es importante tener en cuenta las limitaciones de este operador respecto a los tipos de datos soportados, ya que es probable que tengamos que forzar un casting si nuestro tipo de datos usa valores con menor rango.

    DEF_EXPLICIT_VALUE(Kilometers,longdouble);inlineKilometersoperator""_km(longdoublevalue){returnKilometers{value};}constautodistance=42.0_km;

    Podéis encontrar el ejemplo completo y ejecutable en Coliru.

    Picando Código: Enlaces Ruby – Junio 2021

    $
    0
    0

    RubyColección de los enlaces interesantes sobre Ruby que encontré durante el mes de junio:

    Noticias sobre el lenguaje y programación Ruby general:

    💎En este post Kingsley Silas explica el camino que usa Ruby para encontrar métodos: Ruby usa una “forma” o “patrón” definidos para determinar el método correcto a llamar y el momento correcto para devolver un error “no method erro”, y podemos llamar a esto el “Ruby Method Lookup Path”. En este tutorial, nos vamos a sumergir en la búsqueda de métodos de Ruby. Al final, vas a tener un buen entendimiento de cómo Ruby recorre la jerarquía de un objeto para determinar a qué método te estás refiriendo.

    💎 Takashi Kokubun escribió un artículo que titula Ruby 3 JIT can make Rails faster (o “la compilación just in time de Ruby 3 puede acelerar a Rails”). Las primeras pruebas con el JIT de Ruby 3.0 hacían a Rails más lento. Pero tras 3 años de investigación Takashi encontró cómo mejorar el rendimiento (compilando TODO), aunque todavía falta trabajo y nos avisa que estemos atentos a Ruby 3.1. Pero confirma que Ruby 3.0 es tres veces más rápido (que Ruby 2.0).

    💎 Kirk Haines – quien dio una charla que recomiendo ver en un reciente stream de The Ruby Galaxy– escribió Implementing a Ruby-like #send() in Crystal. En él explica cómo implementar un método send en Crystal usando macros y es un artículo bastante interesante tanto para Rubystas como para “Crystalistas”.

    💎Benoit Daloze escribió un artículo sobre los instaladores de Ruby: rvm, ruby-build y ruby-install y los “cambiadores” de Ruby: rvm, rbenv y chruby. Analiza los pros y los contras de cada uno y termina concluyendo que RVM es la herramienta menos recomendada. Es la que uso en mi día a día, pero he usado varios otros antes (particularmente chruby, rbenv y asdf). De repente debería cambiar de nuevo y ver cómo se integra con Spacemacs…

    💎Maxime Chevalier-Boisvert escribió un artículo súper interesante sobre YJIT: Yet Another Ruby JIT. Desde el año pasado ella trabaja en un equipo de Shopify encargado de mejorar el rendimiento del código Ruby -optimizando el intérprete CRuby y su garbage collector, la implementación de TruffleRuby- y ahora este nuevo compilador JIT dentro de CRuby. Como menciono, es súper interesante tanto el artículo como el proyecto en sí. Ya hay resultados bastante prometedores, y propone mejoras a CRuby que beneficiarían tanto al JIT actual de Ruby 3 (MJIT) como a YJIT y futuras implementaciones. Lectura recomendada – YJIT: Building a New JIT Compiler Inside CRuby. También recomiendo leer el hilo en reddit donde Maxime hizo un AMA espontáneo agregando todavía más detalles al tema.

    💎Una discusión en reddit plantea que “Ruby y Rails no habían tenido nada como Shopify hasta ahora” en lo que se refiere a soporte empresarial. Si bien tienen equipos dedicados tanto a Ruby como a Rails, otras empresas como GitHub, Stripe y Heroku (que le paga el sueldo al mismísimo Matz) también vienen dándole mucho soporte a Ruby. Pero lo interesante es que en el hilo de Reddit comentaron Peter Zhu (Ruby committer que trabaja en Shopify) y Richard Schneeman (contribuye a Rails, mantiene Puma y Sprockets, trabaja en Heroku) entre otros. Del lado de Shopify, Peter Zhu comenta que los proyectos Ruby principales en Shopify actualmente son: YJIT (mencionado en el punto anterior) y Variable Width Allocation (VWA) para MRI y TruffleRuby, una implementación alternativa de Ruby. También comenta que han colaborado directamente con GitHub tanto en Ruby como en Rails. Si bien es difícil medir el aporte de cada empresa, lo importante es que muchas empresas están trabajando y colaborando para hacer Ruby mejor, lo cual le asegura un futuro interesante y sano a mi lenguaje de programación favorito.

    💎 De este último hilo otro recurso interesante: Peter Zhu publicó un artículo sobre Garbage Collection en Ruby, además de tener varios artículos interesantes más sobre Ruby en su blog.

    💎 Y cuanto más compiladores JIT tengamos mejor (?). Hace tiempo Chris Seaton programó uno para Ruby, hecho en Ruby. Nunca se motivó para terminarlo, pero decidió liberarloen GitHub. Sirve como material didáctico, en el README del proyecto mismo dice “Se supone que lo leas, no que lo uses”. Tiene varios experimentos y documentos, así que nos puede ayudar a entender cómo funciona un JIT y aprender más de Ruby y el compilador.

    💎 Se publicó nueva versión de JRuby – 9.2.18.0. La rama 9.2.x es compatible con Ruby 2.5.x y se sincroniza con C Ruby. Se está trabajando activamente en la versión 9.3.0, pero esta versión de la rama anterior corrige algunos problemas. Se mejoró soporte para sub procesos en ambientes puramente Java, arreglos en concurrencia, y la biblioteca de socket fue backporteada de master con todos los arreglos recientes, mejoras en compatibilidad y demás. También se publicó después JRuby 9.2.19.0, una versión de correcciones para un problema con el flag --dev y un problema con zonas horarias en Windows.

    💎Julie Jones, desarrolladora autodidacta, viene publicando en Twitter varios piques de Ruby bajo la premisa de 100 Days Of Code. Entre sus tweets podemos encontrar piques bastante buenos sobre bloques, Procs, y varias cosas más de Ruby. Podemos ver los tweets en este enlace y seguirla en Twitter en @codewithjulie.

    💎 Nat Friedman, CEO de GitHub, twiteó que GitHub procesa 2.8 miles de millones de pedidos a su API por día, con un pico de 55.000 pedidos por segundo. La aplicación es un monolito Rails, confirmado por Raffaele Di Fazio (del equipo de plataforma de GitHub) con la biblioteca Ruby resque para procesos en segundo plano. También hacen deploy a producción entre 20 y 30 veces por día. Son datos bastante interesantes y confirman lo que ya sabemos: Ni Ruby ni Rails están muertos, y Rails sí puede escalar…

    Gemas y bibliotecas

    💎httplog es una gema para loguear pedidos HTTP hechos desde una aplicación Ruby. Funciona con varias bibliotecas y gemas populares como Net::HTTP (v4+), OpenURI, Patron, HTTPClient y HTTParty, pero está parcialmente testeada para Faraday y Typhoeus.

    💎 Se publicó Rubocop 1.17 que corrige un error que hacía que Rubocop fallara usando el bot Layout/HashAlignment, soporte para pattern matching de Ruby 2.7 y mucho más.

    💎Textbringer es nada más y nada menos que un editor de texto inspirado en Emacs, escrito en Ruby. Dos de las mejores cosas del mundo combinadas: Emacs y Ruby 🙇

    En el blog:

    Eventos

    💎 Están publicados los videos de Euruko 2021, la conferencia europea de Ruby: Videos Euruko 2021 Día 1Videos Euruko 2021 día 2

    💎 Se realizó EMEA On Rails, un evento virtual para Rubystas. Participan varios grupos de usuarios de distintos países y podemos ver las charlas y talleres en este enlace.

    💎 El jueves 24 fue el meetup online de Ruby Galaxy. Hablaron Ramón Huidobro y Megan Tiu sobre “MINASWAN” (Matt is nice and so we are nice – Matz es bueno así que somos buenos) ha sido parte de la comunidad Ruby desde su fundación, y una de las cosas más lindas que puedes hacer es entrenar y mentorear a alguien. Este jueves a las 19:00 UTC vamos a estar hablando con dos expertos en entrenamiento y amabilidad. Como siempre, se transmitió a través de Twitch.

    El post Enlaces Ruby – Junio 2021 fue publicado originalmente en Picando Código.

    Blog Bitix: Prueba y opinión sobre jugar mediante streaming

    $
    0
    0

    Cada una de las empresas más importantes en el sector de los juegos ya tienen sus servicios de juego en la nube o mediante streaming, en unos años puede ser la opción preferente de muchos jugadores dejando a las consolas como hardware dedicado a comprar obsoletas. El principal requerimiento del juego mediante streaming es tener una buena conexión a internet que sea estable y con un ancho de banda superior de al menos 35 Mbps para la máxima calidad. Ahora que tengo una conexión con fibra óptica he probado uno de estos servicios, GeForce NOW, por la buena experiencia que he tenido me replanteo las opciones a futuro para un nueva computadora, no teniendo que ser un equipo de más de 700 € ni requerir una tarjeta gráfica dedicada, puede ser de nuevo un pequeño y compacto Intel NUC.

    Hace un tiempo escribí una opinión Sobre la PlayStation 4, PlayStation 5 y Google Stadia donde comentaba que para que triunfen los juegos en la nube o mediante streaming lo único que les falta es tener un catálogo amplio de juegos e incorporar las últimas novedades desde su lanzamiento.

    Aún sabiendo que la PlayStation 5 sería lanzada en poco más de un año al final me compré una PlayStation 4, ya que en ese momento era la opción más viable para mi, tenía ADSL como conexión a internet y uso GNU/Linux con un pequeño computador Intel NUC que no está destinado a juegos con su gráfica integrada poco potente.

    Con el cambio de conexión de ADSL a fibra en mi conexión a internet que tiene un ancho de banda muy superior hace posible la utilización de algunos servicios como vídeo por streaming y también los nuevos servicios de juego por streaming. Ahora he probado uno de ellos, la opción de GeForce NOW para experimentar esta forma de jugar que puede convertirse en el futuro de los juegos y hacer que la PlayStation 5 y las Xbox X/S sean las últimas consolas con hardware dedicado al finalizar su periodo de vida de entre 7 y 10 años, tiempo en el cual el juego el juego por streaming sea adoptada y la opción preferente de muchos jugadores por sus ventajas.

    Plataformas de juego por streaming

    Plataformas de juego por streaming

    Contenido del artículo

    Juegos mediante streaming

    El juego en la nube o por streaming consiste en vez de utilizar una computadora propia para ejecutar el juego hacerlo utilizando una computadora en la nube. Esto es muy diferente al modo de jugar empleado hasta el momento, ya sea con un ordenador o consola pero siempre disponiendo de este hardware dedicado a juegos.

    Con el juego en la nube los juegos se ejecutan en los servidores del servicio de juegos por streaming por lo que no requieren un equipo potente, cualquier ordenador o un portátil bastará incluso uno que no pase de los 200 €. El juego se ejecuta en la nube, los datos de entrada de teclado, ratón o mando se envía al servidor en la nube, se procesan y la imagen y sonido se envían al cliente como si se tratase de un vídeo.

    El juego mediante streaming cambia las reglas no solo por el hardware necesario sino por el modelo de comercialización de los juegos.

    Ventajas

    La principal ventaja del juego mediante streaming es que no requiere un hardware potente para jugar, los juegos son uno de los programas aún con su aspecto dedicado al ocio que requieren más capacidad de cómputo de un ordenador. Un juego requiere una potente tarjeta gráfica para poder jugar a las últimas novedades lanzadas al mercado, pasado un tiempo salen juegos más exigentes y requieren cambiarla por una nueva más potente. El procesador ha de estar acorde a la gráfica, es otra pieza de hardware a renovar pasado el tiempo. En definitiva una computadora dedicada a juegos requiere ser renovada al cabo de unos años, unos 7 años para poder seguir jugando o menos para los más exigentes.

    Un equipo gamer por sus componentes potentes son caros, un equipo gamer nuevo de gama media alta empieza a partir de los 700 € y puede llegar a precios de hasta 2000 €, teniendo en cuenta que que muchos componentes de una computadora antigua no es posible reutilizarlos. Un equipo consta principalmente de procesador, tarjeta gráfica, memoria RAM, placa base, fuente de alimentación y almacenamiento SSD, en un equipo nuevo pasados algunos años hay que renovarlos todos por compatibilidad y para que estén equilibrados.

    Una gráfica potente no es aprovechada con un procesador poco potente, de nada sirve una computadora potente con poca memoria RAM y al renovar la placa base que implementa nuevos estándares requiere renovar todos los demás componentes al mismo tiempo. Solo la tarjeta gráfica de gama media-alta cuesta entre 300 € y 1000 €, el procesador entre 150 € y 400 €, precios a los que hay que sumar el resto de componentes. Dado que en los juegos en la nube no es necesario este hardware no hay que pagar por adelantado este significativo coste, solo se paga por el servicio mientras se usa.

    Otra ventaja de los juegos en la nube, es que no requieren instalación ni actualizaciones con lo que no hay esperas en el momento disponible para jugar, no hay que esperar a que una actualización se descargue y se instale.

    Finalmente, es posible jugar en cualquier ubicación en la que haya disponible una conexión a internet suficiente con cualquier dispositivo que se posea.

    Requerimientos

    El principal requerimiento del juego mediante streaming es que requiere conexión a internet, preferiblemente de fibra que ofrece unos anchos de banda de entre 100 Mbps y 600 Mbps junto con una menor latencia que otras conexiones, en España son lo habitual en nuevas contrataciones y alta de instalaciones. No en todos los países hay este tipo de conexiones pero a medida que pase el tiempo la cobertura será mayor y llegará a más lugares y países.

    Aunque en este video se muestra el juego Cyberpunk 2077 jugando en Stadia con una conexión ADSL de 13 Mbits, la fibra será mejor por tener menor latencia y más ancho de banda que permitirá evitar artefactos, aún así el vídeo muestra que es perfectamente posible jugar con ADSL. La conexión ha de ser estable, en caso de que haya un corte en la conexión el juego es interrumpido ya que funciona a través de internet.

    El segundo requerimiento es un ordenador o un dispositivo compatible con la plataforma. En caso de un ordenador cualquier ordenador, portátil e incluso teléfono inteligente es suficiente, no necesita una tarjeta gráfica dedicada.

    El tercer requerimiento es una aplicación que haga de cliente para el servicio de streaming. Es un programa para el que hay clientes nativos para los sistemas operativos Windows y macOS, para los usuarios de GNU/Linux en vez de un cliente nativo es posible con un navegador Chrome o Chromium, otra opción es con un dispositivo Google Chromecast instalando la aplicación GeForce NOW de la tienda de Android o Stadia.

    Opinión

    Ahora que tengo fibra como conexión a internet he probado el servicio de juego en streaming de Nvidia GeForce NOW. La característica más destacada de GeForce NOW es que es un simplemente un servicio de juego, los juegos se importan de las colecciones de juegos comprados en Steam o Epic Games. Otra propiedad de GeForce NOW es que ofrece crear una cuenta con la que es posible jugar de forma gratuita durante periodos de una hora esperando en una cola con el resto de usuarios de la plataforma a que se libere un servidor. Los usuarios que pagan por el servicio tienen preferencia en la cola y se ofrece mejor calidad gráfica, su coste es de uno 10 €/mes o de 100 € pagando de forma anual.

    A diferencia de Stadia la lista de juegos compatibles con GeForce NOW es bastante amplia, entre las que se encuentran muchos juegos triple A. GeForce NOW no requiere insertar una tarjeta de crédito al crear la cuenta y está disponibles algunos juegos gratuitos y populares con los que probar el servicio. Otra diferencia de Stadia con GeForce NOW es que al comprar un juego en Stadia este solo se puede jugar en Stadia, GeForce NOW ofrece más libertad ya que el juego se compra en la plataforma que se desee y es posible jugarlo por streaming en GeForce NOW o en el PC si se desea en el futuro o dependiendo de la ocasión. Un punto bueno de Stadia es que ofrece dos modalidades, una por la que se compra el juego y se juega en la plataforma sin coste adicional por el servicio con la limitación de solo poder jugarlo a 1080p, que tampoco está mal, o Stadia Pro que es una suscripción pagando por el servicio que da acceso a todo su catálogo de juegos y permite jugarlos a 4K con mayor calidad.

    Aparte de su reducido catálogo de juegos comparado con otras plataformas pero que va creciendo poco a poco dos cosas no me han gustado de Stadia, una que al crear cuenta en Stadia no se puede hacer sin primero suscribirse a la modalidad Stadia Pro lo que requiere introducir una tarjeta de crédito, lo segundo es que sin completar el registro en Stadia no es posible consultar los precios de los juegos para poder comparar si son caros, tiene el mismo que en cualquier otro lugar y si ofrece descuentos de forma regular, esto ha hecho que aún no me haya decidido a probarlo.

    El modelo de comprar los juegos y poder jugarlos en Stadia cuando se tenga tiempo es adecuado si se es un jugador ocasional evitar pagar por la suscripción en periodos que no se use el servicio o de forma intermitente. Si se es un jugador que juega habitualmente varios días de la semana el modelo de suscripción da acceso a todo el catálogo de juegos sin tener que comprarlos individualmente. PlayStation Now permite acceder al catálogo de juegos exclusivos de la PS, Stadia tiene un servicio de suscripción que también da acceso a todo el catálogo de sus juegos y los que se añadan además de poder jugar en 4K o un servicio sin suscripción comprando los juegos sin incurrir en cuotas mensuales, GeForce NOW permite jugar a los juegos de la colección de Steam y Epic Games sin necesidad de un PC gamer y la opción de jugar mediante su capa gratuita.

    Algunos juegos mediante streaming son compatibles con teclado y ratón y otros es posible jugarlos con mando de las consolas PlayStation, Xbox o compatibles. Aún el juego por streaming no es la opción preferente de los jugadores que siguen comprando PC y consolas, sin embargo, las compañías ya están desarrollando sus servicios de juego, tanto Sony con su PlayStation Now y Microsoft con xCloud a las que se unirán otras tecnológicas con sus servicios como Nvidia con GeForce NOW, Google con Stadia y Amazon con Amazon Luna a falta de lo que haga Nintendo o Apple.

    Si hubiese tenido fibra antes de comprar la PlayStation 4 y hubiera probado GeForce NOW o Stadia posiblemente no hubiese comprado la consola. Sin embargo, esto hace que en el futuro descarte comprar un equipo gamer para jugar, en el futuro cuando el Intel NUC se me quede insuficiente seguiré comprando un equipo del mismo formato, si GeForce NOW, Stadia u otra ofrece estas mismas características e incorpora aún más novedades me plantearía comprar una suscripción anual, que me saldría más barato que el equipo gamer. Después de dos años de comprar el Intel NUC y evaluar que cambiaría me en el futuro me planteaba comprar un equipo lo más pequeño posible pero con gráfica dedicada para juegos, al ya tener fibra el juego por streaming es una nueva posibilidad muy buena que me hace replantear las opciones que evaluaba para jugar en el artículo.

    Haciendo cálculos un ordenador dedicado a juegos de gama media o alta tiene un coste de entre 700 a 1200 € pudiendo llegar a 2000 €, una consola entre 300 € y 500 €. Haciendo un cálculo tomando como referencia los precios de GeForce NOW, PlayStation Now y de Stadia Pro que son de 10 €/mes los precios del PC dan para entre 6 y 10 años pagando un servicio de juegos por streaming, en el caso de las consolas entre 2 y 4, en el caso de Stadia existe la posibilidad de pagar solo por los juegos comprados.

    Un punto para mí muy interesante es que por streaming es posible jugar con el dispositivo de entrada que se prefiera, pudiendo ser con teclado y ratón o con un mando compatible con el servicio. Los juegos de tipo de disparo, de estrategia y de rol resultan más cómodos jugarlos con teclado y ratón, en las consolas solo es posible jugar con mando. El hecho de que no necesite mucho hardware la plataforma de juegos por streaming es accesible desde cualquier ubicación en la que haya una conexión a internet, aspecto que algunas personas les resultará interesante si viajan o en los periodos de vacaciones.

    Por otro lado, aun con el gran trabajo de Steam de permitir ejecutar juegos en GNU/Linux que solo están para Windows el streaming evita los problemas de los controladores gráficos e incluso para los usuarios de FreeBSD u otros BSD que son aún más minoritarios y tienen peor soporte de controladores gráficos que en GNU/Linux el streaming es una opción que da acceso a jugar sin ningún tipos de problema de compatibilidad y rendimiento.

    Prueba del servicio GeForce NOW

    El primer paso para probar GeForce NOW es crear una cuenta de NVidia que únicamente requiere una dirección de correo electrónico, la contraseñas de la cuenta, verificar la cuenta de correo electrónico y elegir un apodo para la cuenta, edad e idioma preferido, no hace falta más información personal.

    El siguiente paso es añadir a la biblioteca de GeForce NOW los juegos adquiridos en Steam, Epic Games y GOG, algunos otros que no están en estas tiendas también están disponibles e integrar las cuentas de Steam y Epic Store en GeForce NOW.

    Juegos de Nvidia GeForce NOWJuegos de Nvidia GeForce NOW

    Juegos de Nvidia GeForce NOW

    Después es necesario descargar la aplicación cliente de GeForce NOW adecuada para el sistema operativo e instalarlo o usar el navegador Chrome o Chromium en cualquier sistema operativo también en GNU/Linux. Finalmente, lanzar el cliente, esperar en la cola de espera unos minutos en función del número de usuarios conectados para jugar durante una hora. Para evitar esperas y jugar a mayor calidad está la suscripción con un precio de 10 €/mes.

    Cola de espera en Nvidia GeForce NOW

    Cola de espera en Nvidia GeForce NOW

    Si el juego está en nuestra colección de juegos de Steam o Epic Games y está soportado por GeForce NOW, es posible jugarlo en GeForce NOW. Dado que la Epic Games está regalando cada semana uno o dos juegos completamente gratuitos algunos de muy buena calidad como Control, Metro 2033 Redux, Frostpunk, City Skylines o Tropico 5. Por si fuera poco hay algunos juegos gratuitos compatibles con GeForce NOW como World of Warships, League of Legends, Path of Exile, Albion Online, Counter-Strike: Global Offensive y muchos más. Utilizando esta combinación de GeForce NOW con juegos regalados por Epic Store he probado este servicio de juego por streaming.

    Estas son algunas capturas de pantalla para observar su calidad. Es algo inferior a lo que es una computadora local por tener la imagen algo de compresión pero tampoco nada exagerado y con más calidad que en muchos vídeos de YouTube.

    ControlControlControl

    Metro 2033Metro 2033Metro 2033

    World of WarshipsWorld of WarshipsWorld of Warships

    Trópico 5Trópico 5

    Juegos Control, Metro 2033, World of Warships, Trópico 5

    En cuanto a retardo de respuesta a las acciones de teclado y ratón apenas se nota, la respuesta es casi inmediata y en muchos juegos no es necesario tener la menor latencia. La experiencia de juego es mejor con una computadora propia pero para los no tan exigentes el servicio de juego por streaming es suficiente.

    Finalmente este es un vídeo del inicio del juego World of Warships y Metro 2033.

    El juego en la nube o por streaming cambia las reglas de juego.

    Arragonán: Píldora. Deshabilitar la comprobación del certificado SSL en Maven

    $
    0
    0

    Hace cosa de un par de semanas que he empecé a colaborar con un nuevo equipo de uno de mis clientes. En su caso trabajan en un entorno Java y utilizan Maven para construir sus proyectos. Como tienen librerías internas publicadas en un Nexus, tuve que configurar mi setting.xml para poder tener acceso a ello, hasta ahí todo normal.

    Pero vez lanzando el primer mvn install me dio un error con el certificado SSL, por ser un certificado autofirmado. Así que tras buscar un poco por ahí, encontré que gracias al subproyecto Maven Wagon podemos decirle que ignore esas comprobaciones porque confiamos en el repository manager configurado:

    mvn install -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true
    

    Variable not found: Enlaces interesantes 452

    $
    0
    0
    Enlaces interesantes

    Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

    Por si te lo perdiste...

    .NET Core / .NET

    ASP.NET Core / ASP.NET / Blazor

    Azure / Cloud

    Conceptos / Patrones / Buenas prácticas

      Machine learning / IA / Bots

      Web / HTML / CSS / Javascript

      Visual Studio / Complementos / Herramientas

      Xamarin

      Otros

      Publicado en Variable not found.

      Variable not found: CRUD en Blazor usando el componente DataGrid de Syncfusion

      $
      0
      0

      SyncfusionSemanas atrás echamos un vistazo por encima a Blazor UI Components, los componentes profesionales para Blazor de Syncfusion, y algún tiempo después revisamos más en detalle uno de los grandes protagonistas en el mundo de los componentes, los de creación de gráficas estadísticas o Charts.

      Sin embargo, si hay un tipo de componentes que todos usamos de forma frecuente en nuestras aplicaciones profesionales son, sin duda alguna, los grids o rejillas de datos. Por esa razón, los componentes que de alguna forma simplifican su creación son probablemente los más populares en todas las suites profesionales o gratuitas, sea cual sea la tecnología en la que trabajemos.

      En esta post, centraremos el foco en Blazor DataGrid, la propuesta de Syncfusion para crear interfaces de rejillas de datos de forma sencilla y eficaz para Blazor Server y WebAssembly.

      Pero antes de empezar, recordad que, aunque se trata de un producto comercial, dispone de una licencia community bastante permisiva, que permite que en muchos casos podamos utilizar los componentes de forma totalmente gratuita.

      Nota: lo que estás leyendo es un post patrocinado por Syncfusion, pero en ningún momento han revisado previamente o condicionado de alguna forma su contenido.

      Blazor DataGrid

      Syncfusion Datagrid

      Blazor DataGrid es un completo componente, perteneciente a la familia Blazor Components de Syncfusion, destinado a crear interfaces de visualización y edición de datos en formato tabular.

      Diseñado bajo el concepto mobile-first, proporciona funcionalidades de visualización, consulta y edición de datos, aportando un gran número de características que seguro cubren la mayoría de escenarios:

      • Enlace a datos locales (colecciones o listas en memoria, EF) o remotos (JSON, APIs, OData, WCF).
      • Ordenación, filtrado y paginación de datos.
      • Agrupación y agregación de columnas.
      • Edición de registros en línea, dentro del propio grid, en cuadros de diálogo o en modo bulk/batch.
      • Selección de filas o columnas.
      • Soporte para rejillas maestro-detalle.
      • Personalización visual mediante temas y plantillas custom.
      • Exportación de datos a formatos frecuentes (Excel, CSV y PDF).

      Y todo ello, utilizando técnicas de virtualización de filas y columnas que hacen posible trabajar con conjuntos de datos con miles o millones de filas sin despeinarse.

      Veamos cómo utilizarlo.

      Puesta en marcha

      Como ya hemos visto en otros posts, los componentes de Syncfusion se distribuyen de forma individual a través de NuGet, por lo que sólo tenemos que añadir a nuestros proyectos los que utilicemos. En este caso, para utilizar la rejilla de datos debemos instalar el paquete Syncfusion.Blazor.Grid.

      Pero como también hemos visto en artículos anteriores, antes de usar estos componentes debemos disponer de claves de licencia del producto. Podéis descargar la free trial, que os permitirá trastear con el componente durante 30 días, o bien, si sois desarrolladores individuales o empresas con menos de 5 empleados, registraros para recibir una licencia community, que os dará acceso a todos los componentes sin limitación, de forma totalmente gratuita.

      Sea como sea, una vez registrados, desde el dashboard de Syncfusion podremos obtener una clave de licencia e introducirla en el código de inicialización de nuestra aplicación:

      using Syncfusion.Licensing;
      ...
      public class Program
      {
      public static void Main(string[] args)
      {
      SyncfusionLicenseProvider.RegisterLicense("YOUR-LICENSE-KEY-HERE");
      ...
      }
      }

      También es importante añadir a la etiqueta <head> de la página contenedora de la aplicación (_Host.cshtml en Blazor Server o index.html en WebAssembly) la referencia a la hoja de estilos del tema visual elegido (hay cinco diferentes):

      <head>
      ...
      <link href="_content/Syncfusion.Blazor.Themes/bootstrap4.css" rel="stylesheet" />
      </head>

      También, como es habitual en estos casos, suele ser una buena idea insertar un @using en el archivo _Imports.razor de la raíz del proyecto para que los componentes estén disponibles en todos los archivos .razor de la aplicación:

      @using Syncfusion.Blazor.Grids

      Por último, debemos registrar los servicios de Syncfusion en el inyector de dependencias:

      services.AddSyncfusionBlazor();

      Hecho esto, lo tenemos todo listo para crear nuestra primera rejilla de datos, para lo que utilizaremos el componente <SfGrid> .

      Mostrando datos con el componente <SfGrid>

      Supongamos una fuente de datos bastante simple, como la siguiente:

      public class Person
      {
      public int Id { get; set; }
      public string Name { get; set; }
      public DateTime BirthDate { get; set; }
      public string FavoriteColor { get; set; }
      }

      public class DataServices
      {
      public static List<Person> GetAll()
      {
      var rnd = new Random();
      var people = new List<Person>();
      for (int i = 1; i < 101; i++)
      {
      people.Add(new Person() {
      Id = i,
      Name = $"Person {i}",
      BirthDate = DateTime.Today.AddDays(-rnd.Next(1, 36500)),
      FavoriteColor = ((KnownColor)rnd.Next(1, 150)).ToString()
      });
      }
      return people;
      }
      }

      La rejilla de Syncfusion admite distintos tipos de enlace a fuentes de datos. Aquí vamos a ver el más simple para que podáis entender fácilmente su funcionamiento, pero en la documentación oficial podéis consultar cómo utilizar otros orígenes como Entity Framework, SqlClient, Web API, OData y más.

      Ahora, introduzcamos en una página nuestra primera instancia del grid, el componente <SfGrid>, simplemente indicándole la fuente de datos:

      @page "/"

      <h1>Grid demo</h1>

      <SfGrid DataSource="@_data"></SfGrid>
      @code
      {
      IEnumerable<Person> _data;
      protected override void OnInitialized()
      {
      _data = DataServices.GetAll();
      }
      }

      Como podemos ver en la siguiente captura, dado que no hemos configurado aún nada de la rejilla, <SfGrid> ha tomado varias decisiones por nosotros, mostrando una columna por cada propiedad pública del conjunto de datos:

      Rejilla mostrando todos los datos en forma de tabla

      Obviamente, a falta de más personalización, los datos no están paginados, las columnas usan como encabezado el nombre del campo, y en su contenido el formato por defecto de cada tipo de datos.

      Podemos mejorar todo esto muy fácilmente. Por ejemplo, para activar la paginación basta con establecer el atributo AllowPaging de SfGrid a true, mientras que si queremos definir detalladamente las columnas podemos usar un elemento <GridColumns> y definir en su interior un <GridColumn> por cada columna a mostrar (por ejemplo, observad que ahora no queremos mostrar la columna "Id"):

      <SfGrid DataSource="@_data" AllowPaging="true">
      <GridColumns>
      <GridColumn Field="@nameof(Person.Name)"/>
      <GridColumn Field="@nameof(Person.BirthDate)"
      HeaderText="Birthdate" Format="dd/MM/yyyy"/>
      <GridColumn Field="@nameof(Person.FavoriteColor)"
      HeaderText="Favorite color"/>
      </GridColumns>
      </SfGrid>

      Como podemos ver, sólo con esto la cosa ya habrá mejorado bastante:

      Rejilla con paginación y personalización de columnas

      Esto pinta bien, así que continuemos explorando el componente. Mirando su extensa documentación, vemos que incluye capacidad de ordenar y filtrar por columnas, así que vamos a ello ;)

      Para habilitar la ordenación simplemente hay que establecer a cierto el atributo AllowSorting de forma general, en el componente <SfGrid>, así como en todas las columnas que queremos que sean ordenables.

      También podemos habilitar el filtrado usando el atributo AllowFiltering de la misma forma, y establecer algunas propiedades con <GridFilterSettings>. En el siguiente ejemplo, indicamos que deseamos filtrar mediante un menú, y que las búsquedas sean case insensitive:

      <SfGrid DataSource="@_data"
      AllowPaging="true" AllowSorting="true" AllowFiltering="true">
      <GridFilterSettings Type="FilterType.Menu" EnableCaseSensitivity="true" />
      <GridColumns>
      <GridColumn Field="@nameof(Person.Name)"
      AllowSorting="true" AllowFiltering="true"/>
      <GridColumn Field="@nameof(Person.BirthDate)"
      HeaderText="Birthdate" Format="dd/MM/yyyy"
      AllowSorting="true" AllowFiltering="true" />
      <GridColumn Field="@nameof(Person.FavoriteColor)"
      HeaderText="Favorite color"
      AllowSorting="true" AllowFiltering="true"/>
      </GridColumns>
      </SfGrid>

      Con estos simples cambios, ya tenemos una completa rejilla capaz de mostrar datos, ordenarlos y filtrarlos:

      Rejilla con ordenación y filtros

      ¿Vamos a por el CRUD completo?

      El componente grid de Syncfusion también es capaz de gestionar las operaciones de actualización. Aunque obviamente requiere algo más de trabajo de configuración del componente, el tiempo a dedicar a esto siempre será muy inferior al que supondría implementar una solución por nuestra cuenta.

      Para conseguirlo, lo primero que debemos asegurar es que SfGrid conoce cuál es la columna que actúa como clave primaria de las filas. Para ello es necesario definir la columna, aunque sea invisible, con el atributo IsPrimaryKey de la siguiente forma:

      ...
      <GridColumns>
      <GridColumn Field="@nameof(Person.Id)" IsPrimaryKey="true" Visible="false" />
      ... resto de columnas
      </GridColumns>

      A continuación, vamos a configurar el componente para que habilite las operaciones de creación, actualización y eliminación de filas.

      Para ello, en primer lugar, le añadiremos una barra de herramientas usando el atributo Toolbar de <SfGrid>. Esta toolbares totalmente configurable, aunque de momento le incluiremos sólo comandos predefinidos específicamente para facilitar la manipulación de datos:

      <SfGrid DataSource="@_data"
      AllowPaging="true" AllowSorting="true" AllowFiltering="true"
      Toolbar="@(new List<string>() { "Add", "Edit", "Delete", "Cancel", "Update" })">
      ...

      Aparte, dentro del componente <SfGrid> incluiremos también el componente <GridEditSettings> para configurar las opciones de edición. En este caso, como puede intuirse, permitiremos realizar todas las operaciones, la edición se realizará en un cuadro de diálogo flotante y mostraremos un diálogo de alerta antes de eliminar filas:

      ...
      <GridEditSettings AllowAdding="true" AllowDeleting="true" AllowEditing="true"
      ShowDeleteConfirmDialog="true" Mode="EditMode.Dialog" />

      El código completo quedaría así:

      <SfGrid DataSource="@_data"
      AllowPaging="true" AllowSorting="true" AllowFiltering="true"
      Toolbar="@(new List<string>() { "Add", "Edit", "Delete", "Cancel", "Update" })">

      <GridEditSettings AllowAdding="true" AllowDeleting="true" AllowEditing="true"
      ShowDeleteConfirmDialog="true" Mode="EditMode.Dialog" />
      <GridFilterSettings Type="FilterType.Menu" EnableCaseSensitivity="true" />
      <GridColumns>
      <GridColumn Field="@nameof(Person.Id)"
      IsPrimaryKey="true" Visible="false" />
      <GridColumn Field="@nameof(Person.Name)"
      AllowSorting="true" AllowFiltering="true"/>
      <GridColumn Field="BirthDate"
      HeaderText="Birthdate" Format="dd/MM/yyyy"
      AllowSorting="true" AllowFiltering="true"/>
      <GridColumn Field="FavoriteColor"
      HeaderText="Favorite color"
      AllowSorting="true" AllowFiltering="true"/>
      </GridColumns>
      </SfGrid>

      ¡Y eso es todo! Con algo más de una decena de líneas tenemos en funcionamiento una rejilla completa con paginación, ordenación, filtros, altas, bajas y modificaciones.

      Grid con altas, bajas y modificaciones

      Obviamente, siempre la realidad será más compleja que esto y tendremos que sumergirnos en configuraciones más avanzadas del componente, pero como punto de partida no está nada mal, ¿verdad?

      Como decíamos al principio, el componente Grid para Blazor de Syncfusion es una solución bastante completa, cuyas funcionalidades sobrepasan en mucho lo que podríamos abarcar en un post introductorio como este:

      • Enlace a prácticamente cualquier tipo de fuente de datos.
      • Uso de data annotations para especificar formatos, validaciones y otras características de las columnas.
      • Múltiples opciones de configuración de filas y columnas.
      • Uso de plantillas personalizas para definir exactamente cómo queremos mostrar los datos.
      • Mantenimiento del estado de grids (página actual, orden, filtros...), para conservarlo entre cambios de página.
      • Exportación e impresión de datos.
      • Barras de herramientas y menús contextuales personalizados.
      • Sistema de eventos para introducir lógica personalizada en múltiples puntos.
      • Copiar y pegar filas o celdas.
      • Selección individual o múltiple de filas.
      • Agrupaciones y agregaciones de datos.
      De hecho, con lo que hemos visto aquí sólo hemos rascado levemente su superficie, pero os animo a que echéis un vistazo a su documentación y veáis sus muchas posibilidades.

      Publicado en Variable not found.

      Variable not found: setTimeout(()=>vnf.resume(), 5184000000)

      $
      0
      0

      Como es habitual por estas fechas, me complace (seriamente) informaros de que a partir de la semana  el blog quedará en "modo verano" y dejaré de publicar nuevos contenidos hasta ya entrado septiembre. 

      Durante este periodo procuraré desconectar totalmente durante un par de semanas, y el resto del tiempo al menos poder bajar un poco el ritmo, a ver si puedo disfrutar un poco de esas cosas que dicen que existen fuera de las pantallas ;)

      ¡Nos vemos a la vuelta!

      Playa
      Playa de Costa Ballena, Rota (Cádiz). Imagen: Hotel Elba

      Publicado en Variable not found.

      Blog Bitix: Los niveles de madurez REST, ejemplo con HATEOAS y documentación con Swagger de un servicio con Spring Boot

      $
      0
      0

      Los niveles de madurez de una API implementada con las convenciones REST trata de aplicar los conceptos y semántica de la web y el protocolo HTTP a un servicio web. Muchas APIs que dicen ser REST no cumplen con todos los niveles de madurez para ser considerada RESTful que incluyen HATEOAS para crear enlaces entre los recursos y HAL para codificar los datos. Muchas se quedan en el nivel 2 al hacer uso únicamente de recursos y verbos, llegar a cumplir el nivel 3 para incluir controles hypermedia tiene algunas ventajas adicionales. Spring Boot proporciona soporte para crear una API que soporte el nivel de madurez 3 de REST y Springdoc permite generar la documentación de la API con Swagger.

      Java

      Spring

      Utilizar REST para implementar un servicio es muy común por ser fácil de construir y consumir, se ha convertido en un estándar para los servicios web. REST proporciona acciones para las operaciones, cacheo, redirecciones y delegación, seguridad tanto para cifrado como para autenticación, compatibilidad hacia atrás y evolución de las APIs, escalabilidad y servicios sin estado. A pesar de su amplio uso en realidad no define ninguna especificación, es simplemente una aproximación, estilo y restricciones para construir servicios escalables basados en la web.

      Cómo alternativa a los servicios REST están gRPC y GraphQL que también son capaces de utilizar como medio de transporte el protocolo HTTP pero se basan en aproximaciones diferentes.

      Utilizar el protocolo HTTP no es suficiente para que en servicio se considere que implementa REST de forma completa, al implementar un servicio basado en la semántica del protocolo HTTP y la web hay varios niveles de madurez. Spring Boot ofrece soporte para implementar servicios web que cumplan con todos los niveles de madurez de REST y Springdoc crear la documentación a partir de las anotaciones de Swagger.

      Contenido del artículo

      Los niveles de madurez REST

      REST se basa en los mismos estándares que se utilizan para las páginas web, estos son el protocolo HTTP y los hiperenlaces que construyen la web. El protocolo HTTP tiene una semántica para cada una de sus operaciones que incluyen las diferentes operaciones básicas de CRUD (crear, leer, actualizar y eliminar), códigos de estado para el resultado de la operación y direcciones de los recursos. Las páginas web devuelven HTML, los servicios REST como formato de datos suelen emplear JSON. Los servicios REST son la aplicación de los mismos conceptos de la web a integración de servicios para computadoras, en vez de a humanos o navegadores web.

      Los niveles de madurez de REST son la aplicación de la semántica del protocolo HTTP y la web a los servicios web. Cada uno de estos niveles incluye una aplicación del protocolo HTTP y la web que el servicio REST debe seguir.

      Muchos servicios que se denominan REST no cumplen con todos los niveles de madurez de REST, no es suficiente utilizar HTTP como transporte, utilizar URLs bonitas para los recursos y usar verbos HTTP. No son pocos los servicios que se denominan como REST pero que no implementan todos los niveles de madurez.

      Nivel 0, transporte HTTP

      En este nivel simplemente se usa HTTP como medio de transporte para hacer llamadas remotas sin usar la semántica de la web. Cada petición tiene su propia dirección de endpoint, estas URLs puede que sigan algunas convenciones como utilizar guiones medios para mejorar legibilidad de las URLs, preferiblemente letras en minúsculas y sin extensiones en las URLs, un endpoint puede devolver los datos en el formato solicitado según la cabecera Accept de modo que la extensión es redundante o no es necesaria.

      En este nivel de madurez las URLs suelen incluir verbos que es una mala práctica, como en los siguientes ejemplos.

      1
      2
      3
      
      /addMessage
      /deleteMessage
      /getMessage
      rest-0.txt

      Nivel 1, recursos

      Los recursos son una parte fundamental del protocolo HTTP, cada recurso tiene su propia dirección web, endpoint o URL. Normalmente en una aplicación los modelos corresponden con su propio recurso junto a su propio  endpoint o URL.

      En este nivel se aplican varias convenciones como las URLs no incluyen una / al final de la dirección, una / representa una relación jerárquica entre diferentes recursos, es posible usar singular o plural para los nombres según se prefiera pero de forma consistente.

      Los endpoints en este nivel de madurez son de la siguiente forma.

      1
      2
      
      /messsage
      
      
      rest-1.txt

      Nivel 2, verbos

      Las operaciones que se realizan sobre los recursos son las operaciones de creación, obtención, actualización y eliminación o CRUD. Usando los diferentes verbos del protocolo HTTP es posible asignar a cada uno de ellos las diferentes operaciones básicas de manipulación de datos.

      Si se quiere obtener un elemento concreto de un recurso se realiza una petición al recurso con el verbo GET indicando el identificativo del modelo, si se quieren obtener todos los elementos del recurso se realiza una petición con el verbo GET sin especificar ningún identificativo, si se quiere crear un nuevo elemento en el recurso se utilizar el verbo POST, si se quiere modificar el verbo PUT y si se quiere eliminar el verbo DELETE.

      • POST: verbo utiliza para realizar operaciones de creación sobre un recurso.
      • GET: verbo utiliza para obtener un elemento de la colección o varios elementos de la colección.
      • PUT: verbo utilizado para realizar operaciones de modificación.
      • DELETE: verbo utilizado para realizar operaciones de eliminación.

      Las cabeceras que son parte del protocolo HTTP son metadatos utilizados con diferentes propósitos como indicar en qué formato se quieren los datos en la respuesta o añadir seguridad.

      Los parámetros de las URLs son otra parte del protocolo HTTP que permiten proporcionar argumentos y datos en la propia URL después del símbolo ? en vez de como datos en el cuerpo de la petición. Los parámetros de las consultas son utilizados con diferentes propósitos como especificar los criterios de una búsqueda o propiedades de los datos que se desean como paginación, filtrado u ordenación.

      Otra parte del protocolo HTTP con los códigos de estado, los códigos de estado HTTP son un número que indica el resultado de la operación. Estos son varios de los códigos de estado más comunes:

      • 200: la operación se ha procesado correctamente.
      • 201, CREATED: un nuevo recurso ha sido creado.
      • 204, NO CONTENT: el recurso ha sido eliminado, no se devuelven datos en el cuerpo de la respuesta.
      • 304, NOT MODIFIED: los datos retornados no han cambiado y provienen de una caché.
      • 400, BAD REQUEST: la respuesta es inválida y no puede ser procesada. La descripción del mensaje de error puede ser devuelta en lo datos retornados.
      • 401, UNAUTHORIZED: acceder o manipular el recurso requiere autenticación.
      • 403, FORBIDDEN: el servidor entiende la petición pero las credenciales proporcionadas no permiten el acceso.
      • 404, NOT FOUND: el recurso de la URL no existe.
      • 500, INTERNAL SERVER ERROR: se ha producido un error interno al procesar la petición por un fallo de programación. En la respuesta no se siempre se devuelve una descripción del error, sin embargo en las trazas del servidor debería haber información detallada del error.

      Tanto para enviar datos como obtener datos el formato utilizado es JSON por ser un formato de texto plano y manipulable desde JavaScript en un navegador web.

      Aunque hasta este nivel puede ser suficiente para implementar un servicio y proporcionar la funcionalidad, no es suficiente para considerarlo RESTful, es necesario el siguiente nivel de madurez con los controles hypermedia.

      Nivel 3, controles hypermedia

      Este nivel se divide en dos aspectos, negociación de contenido y descubrimiento de enlaces del recurso. Este es el nivel al que muchas implementaciones de servicios REST no implementan por mayor sencillez aún sin las ventajas que proporcionan los controles hypermedia o por los problemas de los controles hypermedia que si son ignorados ni utilizados no proporcionan ninguna de sus ventajas.

      La negociación del contenido permite al cliente especificar el formato de los datos en los que quiere el resultado. Se solicita con la cabecera Accept en la petición. Por ejemplo, un cliente del servicio REST que desee los datos en formato JSON debe proporcionar una cabecera Accept: application/json y si los desea en formato XML una cabecera Accept: application/xml. En caso de enviar datos en el cuerpo de la petición el formato de los datos proporcionados se especifica con la cabecera Content-Type. En caso de que el servicio no soporte el tipo de datos proporcionado o no sea capaz de proporcionar en el formato solicitado devuelve el código de estado 415 que indica formato de tipo de datos no soportado.

      La web es una colección de páginas web relacionadas a través de enlaces. HATEOAS es el principio que aplica enlaces en los datos de las entidades que permite navegar entre ellas y descubrir las acciones disponibles, un cliente de un servicio REST que implemente HATEOAS no necesita conocer las URLs para interaccionar con las diferentes acciones, estas son devueltas en los datos de la respuesta como metadatos.

      Para obtener los enlaces que ofrece el recurso es necesario hacer una petición y obtener datos, esto es un problema ya que si el cliente ha de conocer de antemano los enlaces o hacer una petición para obtenerlos se anulan parte de las ventajas de HATEOAS, el cliente ha de harcodearlos en su código. Esta acción index permite obtener todos los enlaces que se ofrece en el recurso que el cliente puede utilizar.

      1
      2
      
      #!/usr/bin/env bash
      curl -v http://localhost:8080/message/index
      
      curl-get-index.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
      {    "_links":{      "self":{        "href":"http://localhost:8080/message/index"      },      "getAll":{        "href":"http://localhost:8080/message"      },      "getById":{        "href":"http://localhost:8080/message/{id}",        "templated":true      },      "add":{        "href":"http://localhost:8080/message"      },      "deleteById":{        "href":"http://localhost:8080/message/{id}",        "templated":true      }    }}
      curl-get-index.json

      Al realizar la siguiente llamada al servicio del ejemplo cuando se devuelve una entidad Message el JSON de sus datos incluye una propiedad _links con los enlaces de sus acciones, en este caso realizar la operación de eliminar. La propiedad links es un array de enlaces que tienen la URL y un nombre o identificativo asociado.

      1
      2
      
      #!/usr/bin/env bash
      curl -v http://localhost:8080/message/1
      
      curl-get.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      {"id":1,"text":"Hello World!","_links":{    "self":{      "href":"http://localhost:8080/message/1"    },    "deleteById":{      "href":"http://localhost:8080/message/1"    }}}
      curl-get.json

      Con HATEOAS en vez de que los clientes construyen las URLs de los recursos para hacer peticiones las obtienen de los datos de la respuesta, al mismo tiempo en la respuesta se especifica las acciones posibles de modo que el cliente no necesita implementar lógica para determinar si una operación es posible. La aplicación tampoco necesita construir URLs con interpolación de cadenas para incluir el identificativo de una entidad, el enlace completo es devuelto en los datos. Esto permite a los clientes no tener que implementarlo reduciendo el riesgo de que la lógica de operaciones posibles del servidor y el cliente quede desincronizadas.

      HAL es un formato de tipos de datos que permite codificar no sólo datos sino también controles hypermedia, indicando a los consumidores otras partes de la API a las que llamar. El enlace self indica al propio recurso, el enlace root indica el recurso de la colección, los enlaces add y delete indican dos operaciones posibles.

      Ventajas y problemas de HATEOAS

      Al cambiar la estructura de las URLs se rompe la compatibilidad de la API con versiones anteriores, uno de los beneficios de HATEOAS es que si la estructura de la URL de la API puede cambiar sin afectar a los clientes al describir estos las URLs de forma dinámica.

      Los enlaces devueltos proporcionan al cliente la lista de operaciones que es posible llamar según el estado de la aplicación o la entidad. Esto es útil para los desarrolladores de los clientes dado que no han de duplicar lógica de cuando es posible realizar una operación. En los casos de varias operaciones encadenadas realizadas en varios pasos con HATEOAS la API guía a los clientes hacia el siguiente paso en el flujo proporcionando únicamente los enlaces que son relevantes según el estado de la aplicación.

      La documentación de la API sigue siendo requerida para describir la semántica de cada enlace junto con información como la estructura de los tipos y tipo de contenido.

      En la parte negativa está que HATEOAS añade complejidad a la API, que afecta tanto al desarrollador de la API como al consumidor de la misma. Hay que realizar un trabajo adicional para añadir los enlaces apropiados en cada respuesta según el estado de la entidad. Esto provoca que la API sea más compleja de construir que una API que no implementa HATEOAS.

      Los clientes de la API también tienen complejidad añadida para entender la semántica de cada enlace además de tener y procesar cada respuesta para obtener los enlaces. Los beneficios pueden compensar esta complejidad añadida pero hay que tenerla en cuenta.

      Si la API es pública seguramente algún cliente la use de forma que la usa incorrectamente sin usar el hypermedia, haciendo a HATEOAS inútil.

      Ejemplo de recurso REST con HATEOAS y ejemplo de código

      En el artículo Cómo documentar una API REST con Swagger implementada con Spring Boot incluía como ejemplo un servicio REST que únicamente implementa hasta el nivel de madurez 2 de REST, esta es la revisión del servicio para implementar hasta el nivel 3 incluyendo hypermedia con HATEOAS y HAL.

      Spring HATEOAS proporciona métodos y clases para incluir los enlaces de hypermedia de las entidades que se devuelven como resultado en el servicio. La clase RepresentationModel es una clase base que incluye métodos para añadir los controles hpermedia, la clase EntityModel es utilizada cuando el resultado es para una única entidad, CollectionModel cuando el resultado es una colección de entidades y PagedModel cuando el resultado es paginado.

      Este es la implementación de servicio REST de ejemplo que trata mensajes, permite obtener una lista de mensajes, crear nuevos y eliminar además de una acción para descubrir todos los enlaces del recurso. Para crear los enlaces de hypermedia de HAL que se devuelven en el JSON como respuesta del servicio se delegan en una clase RepresentationModelAssembler.

       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
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      
      packageio.github.picodotdev.blogbitix.springresthateoas;...@RestController@ExposesResourceFor(Message.class)publicclassMessageControllerimplementsMessageApi{    privateMessageModelAssemblerassembler;    privateMap<Long,Message>messages;    publicMessageController(MessageModelAssemblerassembler){        this.assembler=assembler;        this.messages=newHashMap<>();        this.messages.put(1l,newMessage(1l,"Hello World!"));        this.messages.put(2l,newMessage(2l,"Welcome to Blog Bitix!"));    }    @Override    publicResponseEntity<CollectionModel<EntityModel<Message>>>index(){        try{            Collection<Link>links=List.of(                Link.of(linkTo(MessageController.class).toUriComponentsBuilder().path(MessageApi.class.getMethod("index").getAnnotation(GetMapping.class).value()[0]).build().toUriString()).withSelfRel(),                Link.of(linkTo(MessageController.class).toUriComponentsBuilder().path(MessageApi.class.getMethod("getAll").getAnnotation(GetMapping.class).value()[0]).build().toUriString()).withRel("getAll"),                Link.of(linkTo(MessageController.class).toUriComponentsBuilder().path(MessageApi.class.getMethod("getById",Long.class).getAnnotation(GetMapping.class).value()[0]).build().toUriString()).withRel("getById"),                Link.of(linkTo(MessageController.class).toUriComponentsBuilder().path(MessageApi.class.getMethod("add",Message.class).getAnnotation(PostMapping.class).value()[0]).build().toUriString()).withRel("add"),                Link.of(linkTo(MessageController.class).toUriComponentsBuilder().path(MessageApi.class.getMethod("deleteById",Long.class).getAnnotation(DeleteMapping.class).value()[0]).build().toUriString()).withRel("deleteById")            );            returnResponseEntity.ok(CollectionModel.empty(links));        }catch(NoSuchMethodExceptione){            thrownewRuntimeException(e.getMessage(),e);        }    }    @Override    publicResponseEntity<CollectionModel<EntityModel<Message>>>getAll(){        List<Message>entities=messages.entrySet().stream().map(e->e.getValue()).collect(Collectors.toList());        returnResponseEntity.ok(assembler.toCollectionModel(entities));    }    @Override    publicResponseEntity<EntityModel<Message>>getById(Longid){        if(!exists(id)){            thrownewResponseStatusException(HttpStatus.NOT_FOUND,"Message not found");        }        returnResponseEntity.ok(assembler.toModel(messages.get(id)));    }    @Override    publicResponseEntity<Void>add(Messagemessage){        if(exists(message.getId())){            thrownewResponseStatusException(HttpStatus.CONFLICT,"Already exists");        }        if(message.isBlank()){            thrownewResponseStatusException(HttpStatus.BAD_REQUEST,"Invalid data");        }        messages.put(message.getId(),message);        returnResponseEntity.ok().build();    }    @Override    publicResponseEntity<Void>deleteById(Longid){        if(!exists(id)){            thrownewResponseStatusException(HttpStatus.NOT_FOUND,"Message not found");        }        messages.remove(id);        returnResponseEntity.ok().build();    }    privatebooleanexists(Longid){        returnmessages.containsKey(id);    }}
      MessageController.java

      Estos son dos comandos de curl para realizar una petición y obtener datos de una colección de entidades.

      1
      2
      
      #!/usr/bin/env bash
      curl -v http://localhost:8080/message
      
      curl-get-all.sh
       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
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      
      {"_embedded":{    "messages":[      {        "id":1,        "text":"Hello World!",        "_links":{          "self":{            "href":"http://localhost:8080/message/1"          },          "deleteById":{            "href":"http://localhost:8080/message/1"          }        }      },      {        "id":2,        "text":"Welcome to Blog Bitix!",        "_links":{          "self":{            "href":"http://localhost:8080/message/2"          },          "deleteById":{            "href":"http://localhost:8080/message/2"          }        }      }    ]},"_links":{    "self":{      "href":"http://localhost:8080/message/"    },    "add":{      "href":"http://localhost:8080/message"    }}}
      curl-get-all.json

      Los enlaces de hypermedia siguiendo la especificación HAL incluidos en el JSON es posible incluirlos directamente con la clase EntityModel, sin embargo, si la misma entidad es devuelta por varios endpoints para no duplicar código es posible delegar la creación de la representación del modelo en una clase dedicada a esta tarea.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      
      packageio.github.picodotdev.blogbitix.springresthateoas;...@ComponentclassMessageModelAssemblerimplementsRepresentationModelAssembler<Message,EntityModel<Message>>{    @Override    publicEntityModel<Message>toModel(Messagemessage){        returnEntityModel.of(message,                linkTo(methodOn(MessageApi.class).getById(message.getId())).withSelfRel(),                linkTo(methodOn(MessageApi.class).deleteById(message.getId())).withRel("deleteById"));    }    @Override    publicCollectionModel<EntityModel<Message>>toCollectionModel(Iterable<?extendsMessage>entities){        CollectionModel<EntityModel<Message>>model=RepresentationModelAssembler.super.toCollectionModel(entities);        model.add(linkTo(methodOn(MessageController.class).getAll()).withSelfRel());        model.add(Link.of(linkTo(MessageController.class).toUriComponentsBuilder().build().toUriString()).withRel("add"));        returnmodel;    }}
      MessageModelAssembler.java

      En caso de que la API esté detrás de un proxy los enlaces devueltos por las entidades han de ser adaptados, Spring proporciona un filtro que aplicado a la aplicación permite especificar con cabeceras los datos de las URLs.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      
      packageio.github.picodotdev.blogbitix.springresthateoas;...@SpringBootApplicationpublicclassMain{    @Bean    publicForwardedHeaderFilterforwardedHeaderFilter(){        returnnewForwardedHeaderFilter();    }    publicstaticvoidmain(String[]args){        SpringApplication.run(Main.class,args);    }}
      Main.java
      1
      2
      
      #!/usr/bin/env bash
      curl -v -H "X-Forwarded-Host: picodotdev.github.io" http://localhost:8080/message/index
      
      curl-get-index-proxy.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
      {    "_links":{      "self":{        "href":"http://picodotdev.github.io/message/index"      },      "getAll":{        "href":"http://picodotdev.github.io/message"      },      "getById":{        "href":"http://picodotdev.github.io/message/{id}",        "templated":true      },      "add":{        "href":"http://picodotdev.github.io/message"      },      "deleteById":{        "href":"http://picodotdev.github.io/message/{id}",        "templated":true      }    }}
      curl-get-index-proxy.json

      Para usar las clases que ofrecen el soporte para HATEOAS es necesario incluir la dependencia de Spring Boot en el archivo de construcción de Gradle.

       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
      26
      27
      28
      29
      
      plugins{id'java'id'application'id'org.springframework.boot'version'2.5.2'id'com.github.johnrengelman.processes'version'0.5.0'id'org.springdoc.openapi-gradle-plugin'version'1.3.2'}application{group='io.github.picodotdev.blogbitix.springresthateoas'version='0.0.1-SNAPSHOT'sourceCompatibility='11'mainClass='io.github.picodotdev.blogbitix.springresthateoas.Main'}repositories{mavenCentral()}dependencies{implementationplatform('org.springframework.boot:spring-boot-dependencies:2.5.2')implementation'org.springframework.boot:spring-boot-starter'implementation'org.springframework.boot:spring-boot-starter-web'implementation'org.springdoc:springdoc-openapi-webmvc-core:1.5.9'implementation'org.springframework.boot:spring-boot-starter-hateoas'implementation'org.springdoc:springdoc-openapi-ui:1.5.9'implementation'org.springdoc:springdoc-openapi-hateoas:1.5.9'}
      build.gradle

      Documentación con Swagger

      Swagger permite documentar un servicio REST, también incluye soporte para documentar un servicio que cumpla con el principio de hypermedia HATEOAS. Swagger proporciona varias anotaciones que se incluyen en la interfaz del servicio, al procesarlas genera un esquema de la interfaz del servicio con OpenAPI a partir del cual genera la documentación que incluye los endpoints y argumentos, verbos, códigos de respuesta y datos de los modelos. Swagger también permite hacer llamadas a los servicios y obtener el comando curl para hacer la petición desde la línea de comandos.

      La definición de la interfaz del servicio además de las anotaciones de Spring para el servicio REST incluye las anotaciones de Swagger para generar el esquema del servicio en http://localhost:8080/v3/api-docs y generar la documentación en formato HTML accesible en la dirección http://localhost:8080/swagger-ui.html.

       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
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      
      packageio.github.picodotdev.blogbitix.springresthateoas;...@Tag(name="message",description="the message API")@RequestMapping(value="/message",produces={"application/hal+json"})publicinterfaceMessageApi{@Operation(summary="Get resource links",description="Returns resource links",responses={@ApiResponse(responseCode="200",description="Successful operation",links={@Link(name="self",operationId="self"),@Link(name="getAll",operationId="getAll"),@Link(name="getById",operationId="getById"),@Link(name="add",operationId="add"),@Link(name="deleteById",operationId="deleteById")})})@GetMapping(value="/index")ResponseEntity<CollectionModel<EntityModel<Message>>>index();@Operation(summary="Get all messages",description="Returns all messages",responses={@ApiResponse(responseCode="200",description="Successful operation",links={@Link(name="self",operationId="self"),@Link(name="add",operationId="add")})})@GetMapping(value="")ResponseEntity<CollectionModel<EntityModel<Message>>>getAll();@Operation(summary="Get a message by id",description="Return a message",responses={@ApiResponse(responseCode="200",description="Successful operation",links={@Link(name="self",operationId="self"),@Link(name="deleteById",operationId="deleteById")}),@ApiResponse(responseCode="400",description="Invalid id supplied"),@ApiResponse(responseCode="404",description="Message not found")})@GetMapping(value="/{id}")ResponseEntity<EntityModel<Message>>getById(@Parameter(description="Id of message to return",required=true)@PathVariable("id")Longid);@Operation(summary="Adds a message",description="Add a message")@ApiResponses(value={@ApiResponse(responseCode="200",description="Successful operation"),@ApiResponse(responseCode="400",description="Invalid data"),@ApiResponse(responseCode="409",description="Already exists")})@PostMapping(value="")ResponseEntity<Void>add(@Parameter(description="Message to add",required=true)@RequestBodyMessagemessage);@Operation(summary="Deletes a message by id",description="Delete a message")@ApiResponses(value={@ApiResponse(responseCode="200",description="Successful operation"),@ApiResponse(responseCode="400",description="Invalid id supplied"),@ApiResponse(responseCode="404",description="Message not found")})@DeleteMapping(value="/{id}")ResponseEntity<Void>deleteById(@Parameter(description="Id of message to delete",required=true)@PathVariable("id")Longid);}
      MessageApi.java

      Esta es la documentación de Swagger.

      Documentación de servicio REST con Swagger UI

      Documentación de servicio REST con Swagger UI

      Documentación de servicio REST con Swagger UI

      Blog Bitix: Formas de generar un número aleatorio en un rango con Java

      $
      0
      0

      En Java hay varias formas de generar números aleatorios, la clase Random permite generar números aleatorios individuales y desde Java 8 con la adición de streams permite obtener una secuencia de números aleatorios de tamaño determinado o indefinido. La clase Math también permite generar números aleatorios aunque es más recomendable usar la clase Random. Finalmente, en caso de querer un identificativo único universal está la clase UUID que genera números aleatorios de 128 bits que se representan mediante caracteres alfanuméricos.

      Java

      Las computadoras hacen el mejor esfuerzo para ser capaces de generar números aleatorios, para ello hacen uso de la entropía de que disponen para obtener aleatoriedad como datos de entrada que recibe por dispositivos de teclado, ratón o red. Generar números aleatorios es útil en ciertas funcionalidades de programación como la criptografía pero también útil en tareas más sencillas como seleccionar un elemento de un array de forma aleatoria u obtener un número aleatorio entre dos cifras.

      Todos los lenguajes de programación ofrecen funciones de soporte para generar números aleatorios, el lenguaje Java también puede hacerse de varias formas.

      Contenido del artículo

      Generar números aleatorios en un rango

      Java ofrece varias clases y formas para generar números aleatorios, dependiendo de cada una la forma de generar un número aleatorio u obtener un número aleatorio en un rango varía ligeramente.

      Con la clase Random

      La clase Random permite generar números aleatorios con varios métodos según el tipo de datos deseado, en el caso de querer números enteros del tipo int con el método nextInt que devuelve números enteros uniformemente distribuidos entre 0 de forma inclusiva y el límite superior indicado de forma exclusiva.

      Dada la especificación del método nextInt si se desea un número aleatorio entre un rango distinto que no empiece en el 0 hay que realizar una pequeña operación matemática.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      packageio.github.picodotdev.blogbitix.javarandom;...publicclassRandomUtil{    publicstaticintgetInt(intmin,intmax){        returnnewRandom().nextInt(max-min+1)+min;    }    ...}
      RandomUtil-random.java
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      
      packageio.github.picodotdev.blogbitix.javarandom;...publicclassMain{    publicstaticvoidmain(String[]args){        System.out.println("Random");        StringrandomNumbers=IntStream.rangeClosed(1,10)                .mapToObj(i->Integer.toString(RandomUtil.getInt(0,10)))                .collect(Collectors.joining(", "));        System.out.printf("Numbers: %s%n",randomNumbers);        ...    }}
      Main-random.java
      1
      2
      
      Random
      Numbers: 10, 7, 4, 6, 9, 6, 8, 8, 8, 9
      
      RandomUtil-random.out

      Usando un stream

      En el caso de desear una secuencia de números aleatorios la clase Random ofrece soporte para obtener un stream en Java 8 de enteros que son números aleatorios.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      packageio.github.picodotdev.blogbitix.javarandom;...publicclassRandomUtil{    ...    publicstaticIntStreamgetIntStream(intmin,intmax){        returnnewRandom().ints(min,max+1);    }    publicstaticIntStreamgetIntStream(intmin,intmax,intsize){        returnnewRandom().ints(size,min,max+1);    }    ...}
      RandomUtil-stream.java
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      packageio.github.picodotdev.blogbitix.javarandom;...publicclassMain{    publicstaticvoidmain(String[]args){        ...        System.out.println("\nStream");        StringstreamNumbers=RandomUtil.getIntStream(1,10,10)                .mapToObj(i->Integer.toString(i))                .collect(Collectors.joining(", "));        System.out.printf("Numbers: %s%n",streamNumbers);        ...    }}
      Main-stream.java
      1
      2
      
      Stream
      Numbers: 10, 10, 4, 1, 8, 2, 7, 8, 3, 5
      
      RandomUtil-stream.out

      Con la clase Math

      Es más eficiente usar la clase Random pero otra forma posible de generar números aleatorios es con la clase Math. El método random de Math devuelve números aleatorios del tipo double entre 0 de forma inclusiva y 1 de forma exclusiva. Para obtener el número aleatorio hay que hacer una multiplicación y conversión a entero.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      packageio.github.picodotdev.blogbitix.javarandom;...publicclassRandomUtil{    ...    publicstaticintgetIntMath(intmin,intmax){        return(int)(Math.random()*((max-min)+1))+min;    }    ...}
      RandomUtil-math.java
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      packageio.github.picodotdev.blogbitix.javarandom;...publicclassMain{    publicstaticvoidmain(String[]args){        ...        System.out.println("\nMath");        StringmathNumbers=IntStream.rangeClosed(1,10)                .mapToObj(i->Integer.toString(RandomUtil.getIntMath(0,10)))                .collect(Collectors.joining(", "));        System.out.printf("Numbers: %s%n",mathNumbers);        ...    }}
      Main-math.java
      1
      2
      
      Math
      Numbers: 10, 3, 2, 7, 6, 0, 9, 5, 0, 4
      
      RandomUtil-math.out

      Generar un identificativo único universal

      Si se desea generar un identificador único universal para una entidad en vez de un número aleatorio en un rango que tiene posibilidades de repetirse está la clase UUID que genera número únicos de 128 bits que se presentan con caracteres alfanuméricos.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      packageio.github.picodotdev.blogbitix.javarandom;...publicclassRandomUtil{    ...    publicstaticUUIDgetUUID(){        returnUUIDUUID.randomUUID();    }}
      RandomUtil-uuid.java
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      
      packageio.github.picodotdev.blogbitix.javarandom;...publicclassMain{    publicstaticvoidmain(String[]args){        ...        System.out.println("\nUUID");        StringuuidNumbers=IntStream.rangeClosed(1,10)                .mapToObj(i->RandomUtil.getUUID().toString())                .collect(Collectors.joining(", "));        System.out.printf("Numbers: %s%n",uuidNumbers);    }}
      Main-uuid.java
      1
      2
      
      UUID
      Numbers: 1a42ebd3-0c80-4b45-bc88-b774a43e3758, 5a3b0fe4-8101-48cb-aa97-21e1fc2a3169, 9a58541e-58b9-4743-9f48-639e2d8f1dcf, 108e3db7-210b-4ad9-80b2-6838ebcb9109, ec023062-a7a5-4f59-9385-3ac929c6d28c, 612205dc-9bf7-44a3-9573-d8277aafcefa, 508801be-a5cb-4d9e-b1bc-be7c772ddc9a, cdde20d2-7521-417a-a881-1e20acdfecd7, 2ef8aaf7-647e-45c8-b573-eac65cbfd777, cf4d72a4-aac0-4218-944c-000673fe65bc
      
      RandomUtil-uuid.out
      Terminal

      El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
      ./gradlew run


      Blog Bitix: Servidor mock para imitar peticiones y respuestas de servicios HTTP con WireMock

      $
      0
      0

      Los microservicios aportan varias ventajas pero también algunos inconvenientes que si no son manejados generan sus propios problemas. Una dificultad de los servicios por las dependencias entre ellos es poder desarrollarlos y probarlos en local, algunos microservicios son complejos con dependencia sobre bases de datos, sistemas de envío de mensajes u otros servicios. Si un microservicio necesita iniciar en local o en entorno todas sus dependencias el desarrollo se vuelve complejo y lento. Para facilitar el desarrollo una opción es utilizar un servidor mock que imite las respuestas para las peticiones que se necesite de uno o varios servicios.

      Java

      Una aplicación diseñada como una colección de microservicios se compone de múltiples de ellos, unos microservicios son consumidos por otros y a su vez un microservicio consume otros uno o varios.

      Algunas aplicaciones son diseñadas para ofrecer su funcionalidad a través de un API desde el primer momento por su independencia de los clientes que hagan uso de ella. Tener un API permite dar soporte a los múltiples clientes ya sean directamente desde el navegador web, una aplicación nativa de un dispositivo como un teléfono inteligente o incluso para ofrecer a tercera partes de modo que realicen integraciones y automatizaciones según sus necesidades.

      Las ventajas de los microservicios son varios como los anteriores junto a algunos otros adicionales, sin embargo, añaden otros problemas, principalmente el mayor número de elementos que los hacen más complejos comparado con una aplicación monolítica.

      Contenido del artículo

      El servidor mock

      Muchas aplicaciones se basan en microservicios REST haciendo uso del protocolo HTTP y JSON como formato de datos. Un servidor mock es simplemente un servidor web que en caso de los microservicios es utilizado para programar las respuestas para las peticiones que se le hagan según el endpoint invocado, variables en el path, parámetros o cabeceras. Las respuestas programadas incluyen el código de estado, cabeceras devueltas y datos del cuerpo.

      Un servidor de imitación o mock facilita el desarrollo de los microservicios de forma independiente y las pruebas. El servidor mock elimina la dependencia de un servicio real junto con todo el entorno de ejecución que necesite que en el caso de algunos llega a ser notablemente complejo si incluye base de datos, sistemas de mensajería u otros servicios. El servicio es sustituido por una imitación que devuelve las respuestas programadas para cada una de las peticiones.

      Otro caso de uso de un servidor mock es permitir realizar pruebas de código o convertir pruebas de integración en unitarias. Otro uso de un servidor mock es que permite centrarse en el desarrollo de un servicio sin necesidad de usar servicios reales incluso antes de que estos estén implementados si su interfaz está definida.

      Uno de los potenciales riesgos de utilizar un servidor mock es que este no se ajuste a la realidad del servicio real cuando este contenga cambios incompatibles. Un servidor mock permite simular las respuestas de un servicio HTTP lo que facilita las pruebas unitarias de la parte cliente, sin embargo, esto no asegura que el servidor al realizar en las pruebas de integración o en producción cumpla con el contrato que el cliente espera de su API. Para asegurar que el servidor soporta las peticiones esperadas por la parte cliente y devuelve los datos esperados otra forma de pruebas es prueba de contrato o contract testing, Pact es una herramienta de pruebas de contrato que soporta el lenguaje Java.

      Opciones de servidores mock

      Como cualquier otro tipo de herramienta hay múltiples opciones entre las que elegir. La principal característica de todo servidor mock es permitir programar las respuestas según las peticiones, sus diferencias está en el lenguaje de programación en el que están implementadas y su entorno de ejecución necesario así como su tipo de licencia. Algunas ofrecen programar las respuestas a través de una API del lenguaje de programación para el que están destinadas.

      Hay muchas opciones de servidor mock algunas conocidas son MockServer, WireMock, Imposter o Prism. Varias implementadas con JavaScript, otras en Java y algunas incluso se ofrecen en forma de software como servicio para delegar el mantenimiento de la herramienta en una tercera parte.

      Características de WireMock

      WireMock es una opción bastante conocida de servidor para hacer mocking. Ofrece bastante flexibilidad en la forma de aprovisionar las respuestas programadas ya sea a través de un archivo de configuración, peticiones REST una vez iniciado el servidor mock o de forma programática mediante una API de Java. También es bastante flexible en su forma de ejecución pudiendo ser como una aplicación Java independiente, de forma embebida como parte de una aplicación Java como sería el caso de querer utilizarlo para realizar pruebas unitarias o como un contenedor de Docker.

      Ofrece una potente definición de correspondencia entre las peticiones realizadas a través de las URLs, métodos, cabeceras, cookies con diferentes estrategias, también ofrece soporte para generar respuestas en formato JSON o XML pudiendo utilizar plantillas para crear respuestas dinámicas según la petición de entrada.  Otras características que ofrece es soporte para HTTPS, hacer de intermediario o proxy entre la aplicación y el servicio real para peticiones que no están programadas, permitir grabar las respuestas obtenidas por la funcionalidad de proxy, simular errores como tiempos de respuesta elevados y crear flujos de peticiones con escenarios que dependen de estado e interacciones previas.

      Es simple de iniciar y configurar, tiene una documentación suficiente para aprender sus conceptos básicos, configuración junto la documentación completa de la API REST y empezar a usarlo en poco tiempo, está implementado en Java que lo hace adecuado si es el entorno de ejecución utilizado para los microservicios.

      Ejemplo de prueba de WireMock

      De forma oficial el proyecto ofrece un archivo jar ejecutable que inicia el servidor web de WireMock de forma independiente, una vez iniciado expone una API REST a través de la cual es posible aprovisionar las respuestas, el aprovisionamiento y configuración también es posible realizarlo mediante parámetros de inicio. A partir de este archivo jar ejecutable es posible crear una imagen de Docker con el servicio para ejecutarlo en forma de contenedor o en pruebas unitarias o de integración con Testcontainers, alguna persona ya ha creado una imagen de Docker de WireMock.

      Como aplicación independiente

      Este es el comando de inicio de WireMock como aplicación independiente.

      1
      2
      
      $ java -jar wiremock-jre8-standalone-2.29.1.jar 
      
      
      wiremock.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      
      SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
      SLF4J: Defaulting to no-operation (NOP) logger implementation
      SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
       /$$      /$$ /$$                     /$$      /$$                     /$$      
      | $$  /$ | $$|__/                    | $$$    /$$$                    | $$      
      | $$ /$$$| $$ /$$  /$$$$$$   /$$$$$$ | $$$$  /$$$$  /$$$$$$   /$$$$$$$| $$   /$$
      | $$/$$ $$ $$| $$ /$$__  $$ /$$__  $$| $$ $$/$$ $$ /$$__  $$ /$$_____/| $$  /$$/
      | $$$$_  $$$$| $$| $$  \__/| $$$$$$$$| $$  $$$| $$| $$  \ $$| $$      | $$$$$$/ 
      | $$$/ \  $$$| $$| $$      | $$_____/| $$\  $ | $$| $$  | $$| $$      | $$_  $$ 
      | $$/   \  $$| $$| $$      |  $$$$$$$| $$ \/  | $$|  $$$$$$/|  $$$$$$$| $$ \  $$
      |__/     \__/|__/|__/       \_______/|__/     |__/ \______/  \_______/|__/  \__/
      
      port:                         8080
      enable-browser-proxying:      false
      disable-banner:               false
      no-request-journal:           false
      verbose:                      false
      wiremock.out

      Por defecto el servidor mock se inicia en el puerto 8080. Con las siguientes peticiones REST es posible aprovisionar manualmente las respuestas, estas peticiones utilizan la API REST de WireMock. También es posible realizar el aprovisionamiento con archivos de configuración creando una carpeta en el directorio de trabajo de nombre mappings creando archivos con extensión json con el contenido del JSON de cada uno de los mappings.

       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
      26
      27
      28
      29
      30
      31
      32
      33
      
      $ curl -v -X POST http://localhost:8080/__admin/mappings/import --data '{
          "mappings": [{
              "request": {
                  "method": "GET",
                  "url": "/message/1"        },
              "response": {
                  "status": 200,
                  "jsonBody": {
                      "id": 1,
                      "text": "Hello World!"            }
              }
          }]
      }'
      
      $ curl -v -X POST http://localhost:8080/__admin/mappings/import --data '{
          "mappings": [{
              "request": {
                  "method": "POST",
                  "url": "/message",
                  "bodyPatterns" : [{
                      "equalToJson" : {
                          "id": 1,
                          "text": "Hello World!"                }
                  }]
              },
              "response": {
                  "status": 200
              }
          }]
      }'
      curl-wiremock-provision.sh

      Una vez aprovisionado el servidor mock con las respuestas deseadas al realizar peticiones al servidor de WireMock si estás coinciden se devuelven las respuestas, la respuestas incluye el código de estado, las cabeceras y los datos de respuesta tal como fueron aprovisionados. En este caso las peticiones se hacen con el comando curl que simulan las peticiones de una aplicación.

       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
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      
      $ curl -v http://localhost:8080/message/1
      *   Trying 127.0.0.1:8080...
      * Connected to localhost (127.0.0.1) port 8080(#0)> GET /message/1 HTTP/1.1
      > Host: localhost:8080
      > User-Agent: curl/7.78.0
      > Accept: */*
      > 
      * Mark bundle as not supporting multiuse
      < HTTP/1.1 200 OK
      < Matched-Stub-Id: 9acc8318-dff3-4d18-9522-56861eff0ca3
      < Vary: Accept-Encoding, User-Agent
      < Transfer-Encoding: chunked
      < 
      * Connection #0 to host localhost left intact{"id":1,"text":"Hello World!"}
      
      $ curl -v -X POST http://localhost:8080/message --data '{"id":1,"text":"Hello World!"}'
      Note: Unnecessary use of -X or --request, POST is already inferred.
      *   Trying 127.0.0.1:8080...
      * Connected to localhost (127.0.0.1) port 8080(#0)> POST /message HTTP/1.1
      > Host: localhost:8080
      > User-Agent: curl/7.78.0
      > Accept: */*
      > Content-Length: 30> Content-Type: application/x-www-form-urlencoded
      > 
      * Mark bundle as not supporting multiuse
      < HTTP/1.1 200 OK
      < Matched-Stub-Id: 7ef3a405-4435-417d-8579-57497d149f29
      < Vary: Accept-Encoding, User-Agent
      < Transfer-Encoding: chunked
      < 
      * Connection #0 to host localhost left intact
      curl-wiremock-request.sh

      En caso de que la petición no coincida con una aprovisionada se devuelve en error 404.

       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
      26
      27
      
      $ curl -v -H "Accept: application/json" http://localhost:8080/message/2
      *   Trying 127.0.0.1:8080...
      * Connected to localhost (127.0.0.1) port 8080(#0)> GET /message/2 HTTP/1.1
      > Host: localhost:8080
      > User-Agent: curl/7.78.0
      > Accept: */*
      > 
      * Mark bundle as not supporting multiuse
      < HTTP/1.1 404 Not Found
      < Content-Type: text/plain
      < Transfer-Encoding: chunked
      < 
      
                                                     Request was not matched                                               =======================
      
      -----------------------------------------------------------------------------------------------------------------------
      | Closest stub                                             | Request                                                  |
      -----------------------------------------------------------------------------------------------------------------------
                                                                 |
      GET                                                        | GET
      /message/1                                                 | /message/2                                          <<<<< URL does not match
                                                                 |                                                           |
      -----------------------------------------------------------------------------------------------------------------------
      * Connection #0 to host localhost left intact
      curl-wiremock-nomatch.sh

      Modificando la aplicación para que las peticiones las haga al servidor de WireMock la aplicación es posible desarrollarla o probarla sin necesidad del servicio real y sus dependencias.

      Embebido en una aplicación para hacer pruebas unitarias

      En el caso de utilizar WireMock para realizar pruebas unitarias el servidor de WireMock ha de iniciarse y aprovisionarse en el contexto de las pruebas, en este caso para las pruebas unitarias con teses de Junit5.

      En este ejemplo se crea una interfaz de un cliente de una API REST con Retrofit, a partir de esta interfaz Retrofit permite crear una instancia de un objeto que a través de sus métodos y parámetros permite hacer llamadas al servicio REST mediante código Java eliminando los detalles de que en realidad hace una petición HTTP.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      packageio.github.picotodev.blogbitix.javawiremock;...publicinterfaceService{    @GET("/message/{id}")    Call<String>message(@Path("id")Longid);}
      Service.java

      La aplicación en su código crea una instancia del cliente del servicio REST e invoca sus métodos de llamada, dado que el cliente realiza operaciones de red al ejecutarlo al hacer la prueba unitaria si el servicio no está iniciado la comunicación fallará produciendo errores.

       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
      26
      27
      28
      29
      30
      31
      
      packageio.github.picotodev.blogbitix.javawiremock;...publicclassMain{    privateServiceservice;    publicMain(){        this.service=buildService();    }    publicStringgetMessage(Longid)throwsIOException{        returnservice.message(id).execute().body();    }    publicServicebuildService(){        OkHttpClientclient=newOkHttpClient.Builder()                .build();        Retrofitretrofit=newRetrofit.Builder()                .client(client)                .addConverterFactory(ScalarsConverterFactory.create())                .baseUrl("http://localhost:8080/").build();        returnretrofit.create(Service.class);    }    publicstaticvoidmain(String[]args){    }}
      Main.java

      Es en este punto donde entra WireMock que permite simular ese servicio, en este ejemplo de prueba unitaria se inicia el servidor de WireMock, se aprovisiona con la petición esperada y respuesta desea a devolver. Se ejercita el código que se desea probar en este caso el método getMessage de la clase Main que en su implementación hace uso del cliente del servicio REST con Retrofit y que en la prueba invocará al servidor de WireMock. Finalmente, se comprueba que la respuesta del clase Main coincida con la esperada.

       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
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      
      packageio.github.picotodev.blogbitix.javawiremock;...publicclassMainTest{    privatestaticWireMockServerwireMockServer;    @BeforeAll    staticvoidbeforeAll(){        wireMockServer=buildWireMockServer();    }    @AfterAll    staticvoidafterAll(){        wireMockServer.stop();    }    @Test    voidtest()throwsException{        // given
              stubFor(get(urlEqualTo("/message/1"))                .willReturn(aResponse()                        .withHeader("Content-Type","appication/json")                        .withBody("{\"id\": 1, \"text\": \"Hello World!\"}")));        // when
              Stringresponse=newMain().getMessage(1L);        // then
              assertEquals("{\"id\": 1, \"text\": \"Hello World!\"}",response);    }    privatestaticWireMockServerbuildWireMockServer(){        WireMockServerwireMockServer=newWireMockServer(WireMockConfiguration.options().port(8089));        wireMockServer.start();        returnwireMockServer;    }}
      MainTest.java

      Este el archivo de Gradle con las dependencias.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
      plugins{    id'application'}repositories{    mavenCentral()}dependencies{    implementation'com.squareup.retrofit2:retrofit:2.9.0'    implementation'com.squareup.retrofit2:converter-scalars:2.9.0'    testImplementation'org.junit.jupiter:junit-jupiter:5.7.1'    testImplementation'com.github.tomakehurst:wiremock-jre8:2.29.1'}application{    mainClass='io.github.picotodev.blogbitix.javawiremock.Main'}tasks.named('test'){    useJUnitPlatform()}
      build.gradle

      En este caso WireMock se ha usado de forma directa, en el caso de utilizar Spring Boot uno de los proyectos de Spring proporciona librerías para facilitar y hacer compatible el uso de WireMock con JUnit y Spring.

      Terminal

      El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
      ./gradlew test

      Una sinfonía en C#: Charla entre amigos sobre Docker

      $
      0
      0

      Este mes he tenido el privilegio de ser invitado por un grupo de colegas y estudiantes del polo tecnológico de la ciudad de Córdoba en Argentina para hablar sobre Docker, puramente como una charla entre colegas sobre el tema. Y así fue, han sido dos Sábado en los que hemos compartido 90 minutos hablando sobre esta tecnología desde el punto de vista de los desarrolladores y también con muy buenos intercambios de opiniones y preguntas.

      Grabaciones

      Estas son las grabaciones de las dos sesiones. (sabrán disculpar mis malas palabras y malos chiste :D )

      Agradecimientos

      Muchas gracias a los presentes y sobre todo a Eugenio Serrano, ex MVP de Argentina y una persona incansable en generar espacios para compartir y generar comunidad.

      Nos leemos

      Blog Bitix: Integración de servicios y sistemas con Apache Camel

      $
      0
      0

      Apache Camel es una librería específica para realizar tareas de integración que ya proporciona e implementa múltiples protocolos de comunicación, formatos de datos, componentes y patrones de integración. Ya tiene implementada toda esta funcionalidad que no hay que implementar en el caso de una aplicación con código propio. Al ser una librería es posible integrarlo en cualquier tipo de aplicación, en el artículo se muestra cómo utilizarlo en una aplicación de Spring Boot en un ejemplo.

      Apache Camel

      Java

      Las empresas y organizaciones con cierta cantidad de años de vida con mucha probabilidad tiene una gran cantidad de servicios y sistemas con diferentes tecnologías, protocolos de comunicación y formatos de datos. Algunos de esos servicios y sistemas también tendrán varios años de vida, de entre ellos habrá alguno que ya puede ser considerado como obsoleto por la tecnología que utiliza, que ya no recibe soporte de nuevas características y solo es modificado en caso tareas de mantenimiento o en caso de un problema grave de seguridad. Estos servicios heredados aún con su antigüedad siguen siendo importantes por el servicio que prestan.

      Sustituir esos servicios o sistemas heredados por otros nuevos a veces no es lo más adecuado ya que intervienen otros factores como el coste de tiempo requerido para desarrollar los nuevos sistemas que reemplacen a los antiguos, el coste económico, la disponibilidad de trabajadores que lo hagan y también por fiabilidad, cambiar un sistema con sus defectos y limitaciones pero que funciona por uno nuevo que no estará exento de sus propios problemas y defectos es un riesgo para el servicio prestado.

      En vez de sustituir servicios y sistemas por unos nuevos una opción que se suele utilizar es proporcionar una integración. Apache Camel es una herramienta específica para realizar tareas de integración, que también se puede utilizar aún cuando no sea para un servicio o sistema heredado.

      Contenido del artículo

      La librería Apache Camel

      Apache Camel es una librería ligera destinada a realizar tareas de integración entre servicios y sistemas. La de utilizar esta librería sobre realizar una integración con código propio específico para cada integración es que Apache Camel ya proporciona una buena cantidad de funcionalidades sin necesidad de tener que implementarlas.

      A diferencia de las herramientas Enterprise Service Bus o ESB que también sin utilizadas para realizar tareas de integración entre sistemas heterogéneos y que suelen ser herramientas grandes y pesadas, Apache Camel es simplemente una librería muy ligera que es posible utilizarla embebida dentro de otras aplicaciones, por ejemplo dentro de una aplicación de Spring Boot.

      Apache Camel soporta multitud de protocolos de comunicación como HTTP, FTP o JMS, formatos de datos como JSON, XML o CSV e  integración con servicios como AWS, Consul o Twitter entre muchos otros. También ya tiene implementados multitud de patrones de integración como choice, filter, muticast, circuit breaker o bulkead. Otra de sus funcionalidades es que soporta realizar pruebas unitarias.

      Conceptos de Apache Camel

      Apache Camel utiliza varios conceptos. La integración o funcionalidades desarrolladas se modelan como un flujo, ruta o route que comienza a partir de un origen o consumer y se envía a un destino o producer. En este flujo se tratan mensajes o Exchange que contiene además de los datos del mensaje o payload metadatos como cabeceras asociadas. En los diferentes pasos del flujo el Exchange puede sufrir transformaciones con los procesadores o processor y en el que se aplican los diferentes patrones de integración o integration patterns.

      Una parte importante de Apache Camel que lo hacen fácil de utilizar son los endpoints que son URLs compuestas de un esquema, contexto y opciones. Un ejemplo de endpoint es el siguiente del componente RabbitMQrabbitmq:exchange para tomar como fuente o destino colas de mensajes o del componente Filefile:misc/ para el sistema de archivos.

      Los flujos se modelan con un lenguaje de dominio específico o DSL ya sea definiéndolo con código Java o en un archivo con formato XML. Al utilizar código Java se gana el soporte del entorno integrado de desarrollo, asistencia de código y detección de errores de compilación.

      Al igual que en una aplicación de Spring existe el ApplicationContext, Apache Camel posee un contexto a modo de registro con todos los objetos de la instancia de Camel.

      La colección de componentes de Apache Camel es muy numerosa.

      Los formatos de datos que soporta también son muy numerosos.

      También soporta los patrones de integración identificados en el libro Enterprise Integration Patterns que ya han demostrado su utilidad para solventar y simplificar los problemas a los que están dirigidos.

      Patrones de integración

      Algunos de los patrones básicos que soporta Apache Camel son choice para elegir rutas alternativas a las que dirigir los mensajes, filter para descartar los mensajes que no cumplan alguna condición, multicast para enviar un mensaje a varios destinos, recipient list para enviar a varios destinos de forma dinámica o wire tap para inspeccionar los mensajes sin alterar su flujo normal. Esos son solo unos pocos patrones de integración soportados.

      Patrón content based routerpatrón filter

      Patrón multicastPatrón recipient list

      Diferentes patrones de integración

      Ejemplo básico con Apache Camel

      Apache Camel al ser una librería es muy fácil de integrarlo en cualquier tipo de aplicación, en este ejemplo se utiliza Spring Boot. El ejemplo consiste en dos rutas, una que simplemente muestra en la salida los mensajes que se envía, la otra ruta lee los archivos CSV de un directorio que contienen listas de productos en diferentes columnas, filtra los productos que no tienen un importe superior a una cantidad, los transforma y les añade el IVA y finalmente los muestra en la salida, cada vez que en el directorio se añade un CSV se procesa.

      Esta es la definición de varias rutas con su DSL en código Java que se definen en las clases que implementan la interfaz RouteBuilder, utilizando Spring definiéndose como un componente son añadidos de forma automática al contexto de Apache Camel. En la ruta HelloWorldRoute simplemente tomo como fuente lo que llega al endpoint de nombre direct:helloworld y lo dirige a la salida del sistema con stream:out sin ningún procesamiento adicional entre el origen y el destino.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      packageio.github.picodotdev.blogbitix.holamundoapachecamel;importorg.apache.camel.builder.RouteBuilder;importorg.springframework.stereotype.Component;@ComponentpublicclassHelloWorldRouteextendsRouteBuilder{    @Override    publicvoidconfigure()throwsException{        from("direct:helloworld").routeId("helloworld").to("stream:out");    }}
      HelloWorldRoute.java

      Al inicio del programa se envía al consumidor de la ruta helloworld diez UUID.

       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
      26
      27
      28
      29
      
      packageio.github.picodotdev.blogbitix.holamundoapachecamel;importjava.util.UUID;importjava.util.stream.IntStream;importorg.apache.camel.ProducerTemplate;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.ApplicationArguments;importorg.springframework.boot.ApplicationRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublicclassMainimplementsApplicationRunner{    @Autowired    privateProducerTemplateproducerTemplate;    @Override    publicvoidrun(ApplicationArgumentsargs)throwsException{        IntStream.range(0,10).forEach(i->{            producerTemplate.sendBody("direct:helloworld",UUID.randomUUID());        });    }    publicstaticvoidmain(String[]args){        SpringApplication.run(Main.class,args);    }}
      Main.java

      La siguiente ruta es algo más compleja y muestra varias de las capacidades de Apache Camel. Monitoriza un directorio con un archivo en formato CSV, cuando este se crea o está presente al iniciar la aplicación la ruta lo toma como fuente de datos e inicia su procesamiento en el flujo.

      Primeramente se procesan los datos transformándolos en objetos Java de tipo Book que son simplemente objetos POJO con una propiedad por cada columna del CSV. Al procesar los datos se obtiene una lista de objetos de tipo Book, con la operación split, la lista de divide en objetos individuales en el flujo.

      Posteriormente, se aplica una condición sobre los objetos, según si el objeto cumple la condición o no se envían a un destino u otro. Según el destino al que están dirigidos se establece un con una cabecera que se transmiten como metadato al mismo tiempo que los datos.

      Finalmente, los mensajes llegan al destino direct:books-stream-out, se aplica un filtro sobre la cabecera anterior, si la cumple se aplica un procesamiento al mensaje para aplicar el IVA sobre el precio del libro y una transformación que cambia el tipo del mensaje de Book a una cadena String, para terminar la cadena se envía a stream:out para imprimirlo en la salida de la aplicación.

      Una vez procesado el CSV con éxito Apache Camel lo mueve a una carpeta oculta .camel, si el mismo archivo es vuelto a copiar en la capeta se procesa de nuevo.

       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
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      
      packageio.github.picodotdev.blogbitix.holamundoapachecamel;importjava.math.BigDecimal;importorg.apache.camel.Exchange;importorg.apache.camel.Processor;importorg.apache.camel.builder.PredicateBuilder;importorg.apache.camel.builder.RouteBuilder;importorg.apache.camel.model.dataformat.BindyType;importorg.springframework.stereotype.Component;@ComponentpublicclassBooksRouteextendsRouteBuilder{    @Override    publicvoidconfigure()throwsException{        from("file:misc/").routeId("books-file")                .unmarshal().bindy(BindyType.Csv,Book.class)                .split(body())                .choice()                .when(simple("${body.price} < 30")).to("direct:books-cheap")                .otherwise().to("direct:books-expensive");        from("direct:books-cheap").routeId("books-cheap").setHeader("type",constant("cheap")).to("direct:books-stream-out");        from("direct:books-expensive").routeId("books-expensive").setHeader("type",constant("expensive")).to("direct:books-stream-out");        from("direct:books-stream-out").routeId("books-stream-out")                .filter(header("type").isEqualTo("cheap"))                .process(newVatProcessor())                .transform(simple("${body.title} at only ${body.price} €"))                .to("stream:out");    }    privateclassVatProcessorimplementsProcessor{        @Override        publicvoidprocess(Exchangeexchange)throwsException{            Bookbook=(Book)exchange.getMessage().getBody();            BigDecimalpriceWithVat=book.getPrice().multiply(newBigDecimal("1.04"));            book.setPrice(priceWithVat);        }    }}
      BooksRoute.java
       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
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      
      packageio.github.picodotdev.blogbitix.holamundoapachecamel;importjava.math.BigDecimal;importorg.apache.camel.dataformat.bindy.annotation.CsvRecord;importorg.apache.camel.dataformat.bindy.annotation.DataField;@CsvRecord(separator=",",crlf="UNIX")publicclassBook{    @DataField(pos=1)    privateStringtitle;    @DataField(pos=2)    privateStringurl;    @DataField(pos=3)    privateBigDecimalprice;    publicStringgetTitle(){        returntitle;    }    publicvoidsetTitle(Stringtitle){        this.title=title;    }    publicStringgetUrl(){        returnurl;    }    publicvoidsetUrl(Stringurl){        this.url=url;    }    publicBigDecimalgetPrice(){        returnprice;    }    publicvoidsetPrice(BigDecimalprice){        this.price=price;    }}
      Book.java
      The DevOps Handbook,https://www.amazon.es/DevOPS-Handbook-World-Class-Reliability-Organizations/dp/1942788002/,23
      The Phoenix Project,https://www.amazon.es/Phoenix-Project-Devops-Helping-Business/dp/1942788290/,25
      The Unicorn Project,https://www.amazon.es/Unicorn-Project-Developers-Disruption-Thriving/dp/1942788762/,24
      Site Reliability Engineering,https://www.amazon.es/Site-Reliability-Engineering-Betsy-Beyer/dp/149192912X/,43
      books.csv

      Esta es la salida del programa.

       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
      26
      27
      28
      29
      30
      31
      32
      
        .   ____          _            __ _ _
       /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
      ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
       \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
        '  |____| .__|_| |_|_| |_\__, | / / / /
       =========|_|==============|___/=/_/_/_/
       :: Spring Boot ::                (v2.5.3)
      
      2021-08-05 20:38:55.887  INFO 13543 --- [           main] i.g.p.b.holamundoapachecamel.Main        : Starting Main using Java 11.0.10 on archlinux with PID 13543 (/home/picodotdev/Documentos/Software/personal/blog-ejemplos/HolaMundoApacheCamel/app/build/classes/java/main started by picodotdev in /home/picodotdev/Documentos/Software/personal/blog-ejemplos/HolaMundoApacheCamel/app)
      2021-08-05 20:38:55.889  INFO 13543 --- [           main] i.g.p.b.holamundoapachecamel.Main        : No active profile set, falling back to default profiles: default
      2021-08-05 20:38:57.146  INFO 13543 --- [           main] c.s.b.CamelSpringBootApplicationListener : Starting CamelMainRunController to ensure the main thread keeps running
      2021-08-05 20:38:57.155  INFO 13543 --- [           main] o.a.c.impl.engine.AbstractCamelContext   : Routes startup summary (total:5 started:5)
      2021-08-05 20:38:57.156  INFO 13543 --- [           main] o.a.c.impl.engine.AbstractCamelContext   :     Started books-file (file://misc/)
      2021-08-05 20:38:57.156  INFO 13543 --- [           main] o.a.c.impl.engine.AbstractCamelContext   :     Started books-cheap (direct://books-cheap)
      2021-08-05 20:38:57.156  INFO 13543 --- [           main] o.a.c.impl.engine.AbstractCamelContext   :     Started books-expensive (direct://books-expensive)
      2021-08-05 20:38:57.156  INFO 13543 --- [           main] o.a.c.impl.engine.AbstractCamelContext   :     Started books-stream-out (direct://books-stream-out)
      2021-08-05 20:38:57.156  INFO 13543 --- [           main] o.a.c.impl.engine.AbstractCamelContext   :     Started helloworld (direct://helloworld)
      2021-08-05 20:38:57.156  INFO 13543 --- [           main] o.a.c.impl.engine.AbstractCamelContext   : Apache Camel 3.11.0 (camel-1) started in 234ms (build:31ms init:190ms start:13ms)
      2021-08-05 20:38:57.161  INFO 13543 --- [           main] i.g.p.b.holamundoapachecamel.Main        : Started Main in 1.629 seconds (JVM running for 1.958)
      26723c08-c1dc-4459-9afa-4a746cad97c8
      31e93fa7-9df8-45fa-9852-36deb677b505
      f629d26d-396a-4db9-a88e-6f072cde7abe
      1c2593e8-09e3-4dcd-becb-9fd6053f5bde
      79087761-6d61-4dfa-8a9b-acd6ad83389f
      a77b1048-3ea1-46cb-b427-1d5b92d556f6
      7e45f1e9-5725-4514-b3cf-cfbf3a406c9a
      17e0999a-4d0f-4724-ab14-dd655b7bb991
      e999a300-d1c2-4911-8d8f-1f0ad575e4e6
      f5b3fb7c-1e22-405b-991b-978bd2e8134f
      The DevOps Handbook at only 23.92 €
      The Phoenix Project at only 26.00 €
      The Unicorn Project at only 24.96 €
      System.out

      Para su ejecución se utiliza la herramienta de construcción Gradle con el siguiente archivo donde se definen las dependencias del proyecto. La librería de Apache Camel para Spring Boot proporciona la funcionalidad de la que la aplicación se mantenga en funcionamiento tal como ocurre cuando se utiliza la dependencia de Spring para desarrollar aplicaciones web.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      
      plugins{    id'application'}repositories{    mavenCentral()}dependencies{    implementationplatform('org.springframework.boot:spring-boot-dependencies:2.5.3')    implementationplatform('org.apache.camel.springboot:camel-spring-boot-bom:3.11.0')    implementation'org.springframework.boot:spring-boot-starter'    implementation'org.apache.camel.springboot:camel-spring-boot-starter'    implementation'org.apache.camel:camel-stream:3.11.0'    implementation'org.apache.camel:camel-bindy:3.11.0'    implementation'org.apache.camel:camel-csv:3.11.0'}application{    mainClass='io.github.picodotdev.blogbitix.holamundoapachecamel.Main'}
      build.gradle
      Terminal

      El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
      ./gradlew run

      Coding Potions: ¿Cuánto cobra un programador en España en 2021?

      $
      0
      0

      Introducción

      Hola, hace unos meses realicé en mi cuenta de Twitter una encuesta en la que preguntaba sobre salarios en el sector de la programación.

      Pues bien, hoy os traigo los resultados de esa encuesta.

      Antes de empezar me gustaría decir que este análisis no busca ser preciso y exacto, es más que nada para poder hacernos una idea de cómo esta la cosa en cuanto a salarios en España para poder tomar mejores decisiones a la hora de negociar el sueldo.

      También me gustaría dar las gracias a mi amigo Guillermo Adán por crear las gráficas que aparecen en este artículo.

      Te recomiendo hacer click derecho sobre las imágenes y abrir en nueva pestaña para que se vean en grande.

      Por cierto, te puedes bajar la infografía mejor maquetada en formato PDF en este enlace.

      📊 Descarga infografía salarios España 2021

      Perfil de la muestra

      La muestra de datos se ha realizado mediante encuesta anónima y se ha difundido por redes sociales. Han respondido un total de 299 personas.

      La muestra total contiene personas de 18 países y 11 áreas profesionales.

      De la muestra total hemos validado 263 tras un filtrado en base a la región a la que pertenecen (Europa) y la moneda en la que cobran su salario (EUR).

      Quiero pedir disculpas a todos los que respondieron en divisas diferentes a euros por quedarse fuera del estudio. Esto ha sido fallo mío y es que por desgracia no indiqué que el salario lo metieran todos en la misma divisa. El año que viene cuando repita esta encuesta.

      Lugar de trabajo

      Como era de esperar la mayoría de perfiles se encuentran en la Comunidad de Madrid (87), seguido por Andalucía (40) y Barcelona (28).

      También hay que destacar que hay un número significativo de perfiles fuera de estas comunidades antes mencionadas, seguramente potenciado por el teletrabajo y la deslocalización de las empresas aprovechando la pandemia.

      Cargo del encuestado

      De la muestra filtrada de 263 perfiles, 112 son seniors, 57 middle (ni senior ni junior), 44 son juniors, 29 son lead y el resto se reparte entre CTO, head y becario. Me sorprende que solo hayan respondido 2 becarios.

      La mayoría de los perfiles corresponden a desarrolladores seniors que trabajan en PYMES y consultoras ( y en menor medida en multinacionales).

      Diferencias de salario entre mujeres y hombres

      De la muestra analizada, 34 son respuestas de mujeres, 223 de hombres y 5 han preferido no responder.

      Aquí tengo que decir que ha sido fallo mío también por no intentar que la muestra quede más igualada. La compartí unicamente en redes sociales pensado que con eso valdría.

      El saliario más alto en hombres se sitúa en 150k euros al año (aunque solo ha habido una respuesta de este estilo) y el más alto entre las mujeres ha sido de 52k.

      Del otro lado, el salario más bajo en hombres ha sido de 8.4k y de mujeres de 6k.

      La mediana de salarios en mujeres ha sido de unos 25k al año y de 38k para los hombres.

      Se pone de manifiesto por tanto el famoso techo de cristal. De los encuestados no hay muestras de mujeres en altos cargos. Por suerte, en la gráfica también se puede apreciar que en los últimos 5 años han habido bastantes incorporaciones de mujeres al sector del desarollo.

      Además de una menor inserción en el desarrollo, las mujeres necesitan más tiempo para acceder a puestos directivos en las compañias.

      Felicidad de los trabajadores respecto a su salario

      Podemos apreciar divergencias en la satisfacción profesional respecto a su salario dependiendo del cargo de los perfiles.

      Los perfiles más juniors en general se sienten más descontentos con su salario, pero la situación va mejorando según ascendemos en la escala de puestos de la compañia.

      Curiosamente, a pesar de ser los perfiles mejor pagados, los puestos de Head y CTO rompen la tendencia a la baja de insatisfacción salarial.

      Conclusiones

      ¿Te han sorprendido los resultados? ¿Ha cambiado tu percepción sobre los salarios en España?

      Por cierto, si quieres los datos en bruto te los puedes descargar aquí:

      Datos en bruto de la encuesta

      Tienes mi permiso para usarlos para hacer análisis de datos o lo que quieras, siempre que menciones a @CodingPotions.

      Mi idea como digo es hacer esta misma encuesta cada año (intentando solucionar los errores que he expuesto antes) para así poder comparar año tras año cómo van cambiando las tendencias salariales.

      Blog Bitix: Introducción y ejemplo de contract testing con Pact

      $
      0
      0

      Al realizar un cambio en un API hay que ser consciente de que los cambios sean compatibles hacia atrás, de lo contrario algunos clientes de la API es probable que dejen de funcionar o tengan un comportamiento erróneo. Para asegurar que los cambios sean compatibles hacia atrás se realizan pruebas unitarias automatizadas de contrato, en Java una opción es Pact para pruebas de contrato de APIs REST.

      Pact

      Java

      Las aplicaciones que ofrecen una API establecen un contrato con los consumidores, los consumidores al usar la API crean una dependencia. Para que un cambio API sea compatible hacia atrás no debe requerir cambios en los consumidores, si el cambio en la API requiere cambios en los consumidores estos corren el riesgo de dejar de funcionar correctamente. Los cambios no compatibles hacia atrás son un problema ya que requieren coordinar el cambio con los consumidores, los desarrolladores de la API tienen control sobre el proveedor pero en algunos casos no sobre los consumidores que deben ser adaptados por sus propietarios.

      Idealmente todos los cambios deberían ser compatibles hacia atrás, sin embargo, en ocasiones no queda más alternativa que introducir un cambio no compatible. Para evitar el problema una opción es versionar la API de tal modo que los nuevos consumidores utilicen la nueva API y los consumidores de una versión anterior tengan un tiempo para adaptarse a la nueva API, durante un tiempo la API antigua y la nueva funcionan simultáneamente, pasado un tiempo y cuando los consumidores hayan pasado a usar la nueva API la versión antigua se elimina.

      REST también es una forma de API en este caso ofrecida a través del protocolo HTTP y habitualmente con JSON con formato de datos, al hacer cambios en una API REST el principio de que el cambio sea compatible hacia atrás se aplica. En REST la API está formada por las direcciones de los endpoints, los parámetros de consulta, las cabeceras de la petición y de respuesta, los códigos de estado de respuesta  y los datos devueltos así como el formato de datos devueltos.

      Cambios compatibles hacia atrás son añadir un nuevo campo aceptado en la petición si no es obligatorio o devuelto en la respuesta o un nuevo parámetro de consulta o un nuevo endpoint. Cambios no compatibles son por el contrario eliminar un campo en la respuesta o eliminar un endpoint. Para posibilitar cambios en una API también se suele utilizar el patrón primero expandir luego contraer o expand-contract con la cual primero se aplican cambios que añaden cosas y posteriormente cuando dejan de usarse se eliminan los que ya no se utilizan, este mismo patrón es aplicable a otras áreas como por ejemplo cambios en las bases de datos.

      Para garantizar que los cambios realizados en una API no introduzcan problemas de compatibilidad hacia atrás se realizan pruebas de contrato. Son especialmente útiles cuando el equipo encargado de la parte productora es distinto del equipo de la parte consumidora ya sea en una misma empresa o de empresas diferentes.

      Contenido del artículo

      Las pruebas de contrato

      En el caso de las API con REST para garantizar que tanto el consumidor y el productor son compatibles a veces se realizan pruebas de integración o pruebas end-to-end o E2E, sin embargo, estas son costosas de realizar en tiempo y esfuerzo requerido. Para simplificar y automatizar estas pruebas de integración una opción es realizar pruebas de contrato.

      Las pruebas de contrato consisten en primera instancia en que el consumidor define las interacciones que necesita, las codifica en un servidor mock que imita las respuestas del productor, realiza las pruebas unitarias y se genera un contrato con las interacciones requeridas para la parte productora.

      Con el contrato generado por el consumidor las interacciones se reproducen en la parte productora, se comparan las respuestas del productor con las requeridas por el consumidor y si coinciden el productor cumple el contrato que requiere el consumidor.

      Las pruebas de contrato permiten convertir las pruebas de integración en pruebas unitarias, para ello separa las pruebas del consumidor y las pruebas de productor. Una herramienta de pruebas de contrato es Pact.

      La herramienta Pact

      Pact es una herramienta para realizar pruebas de contrato que soporta el lenguaje Java con la librería JUnit para realizar pruebas unitarias entre otros lenguajes.

      Pact en la parte consumidor también hace las funciones de servidor sin embargo adicionalmente el servidor mock de WireMock permite guardar esas interacciones y realizar las pruebas para la parte productora. Esto permite detectar problemas de que un cambio introduzca problemas de incompatibilidad y poder probar de forma desacoplada el consumidor y productor.

      Pruebas de contrato con Pact

      Pruebas de contrato con Pact

      Ejemplo de contract testing con Pact

      Este ejemplo consiste  en un endpoint REST programado usando Spring Boot que acepta un argumento opcional en la ruta y un parámetro de consulta. La respuesta consiste simplemente en un mensaje en forma de cadena que varía según la cabecera Accept-Language.

       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
      26
      27
      28
      29
      30
      31
      32
      
      packageio.github.picodotdev.blogbitix.javapact;...@RestControllerpublicclassRestService{    privatestaticfinalMap<String,String>MESSAGES;    static{        MESSAGES=newHashMap<>();        MESSAGES.put("es-ES;default","¡Hola mundo!");        MESSAGES.put("es-ES;hello","¡Hola %s!");        MESSAGES.put("en-GB;default","Hello World!");        MESSAGES.put("en-GB;hello","Hello %s!");    }    @GetMapping(path={"/message","/message/{name}"})    publicStringmessage(@RequestHeader(value="Accept-Language",defaultValue="en-GB")Stringlocale,@PathVariable(required=false)Stringname,@RequestParam(name="random",required=false)Stringrandom){        System.out.printf("Random: %s%n",random);        Stringmessage="";        if(name==null||name.isBlank()){            Stringkey=String.format("%s;default",locale);            message=MESSAGES.getOrDefault(key,MESSAGES.get("en-GB;default"));        }else{            Stringkey=String.format("%s;hello",locale);            Stringvalue=MESSAGES.getOrDefault(key,MESSAGES.get("en-GB;default"));            message=String.format(value,name);        }        returnmessage;    }}
      RestService.java

      El consumidor del servicio está implementado usando la librería Retrofit para crear el cliente que abstrae de las llamadas HTTP.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      packageio.github.picodotdev.blogbitix.javapact;...publicinterfaceService{    @GET("/message")    Call<String>message(@Header("Accept-Language")StringacceptLanguage,@Query("random")Stringrandom);    @GET("/message/{name}")    Call<String>message(@Path("name")Stringname,@Header("Accept-Language")StringacceptLanguage,@Query("random")Stringrandom);}
      Service.java
       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
      26
      27
      
      packageio.github.picodotdev.blogbitix.javapact;...publicclassServiceClientimplementsService{    privateServiceservice;    publicServiceClient(OkHttpClientclient,StringbaseUrl){        Retrofitretrofit=newRetrofit.Builder()                .client(client)                .addConverterFactory(ScalarsConverterFactory.create())                .baseUrl(baseUrl).build();        this.service=retrofit.create(Service.class);    }    @Override    publicCall<String>message(StringacceptLanguage,Stringrandom){        returnservice.message(acceptLanguage,random);    }    @Override    publicCall<String>message(Stringname,StringacceptLanguage,Stringrandom){        returnservice.message(name,acceptLanguage,random);    }}
      ServiceClient.java

      Pruebas unitaria del consumidor

      En los casos de prueba se codifican las interacciones esperadas por el cliente que son proporcionadas por Pact en un servidor mock, las pruebas unitarias usan el cliente HTTP con la dirección del servidor mock de Pact que es proporcionado como un parámetro en los métodos de test.

        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
       26
       27
       28
       29
       30
       31
       32
       33
       34
       35
       36
       37
       38
       39
       40
       41
       42
       43
       44
       45
       46
       47
       48
       49
       50
       51
       52
       53
       54
       55
       56
       57
       58
       59
       60
       61
       62
       63
       64
       65
       66
       67
       68
       69
       70
       71
       72
       73
       74
       75
       76
       77
       78
       79
       80
       81
       82
       83
       84
       85
       86
       87
       88
       89
       90
       91
       92
       93
       94
       95
       96
       97
       98
       99
      100
      101
      102
      103
      
      packageio.github.picodotdev.blogbitix.javapact;...@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT,classes=Main.class)@ExtendWith(PactConsumerTestExt.class)@PactTestFor(providerName="serviceProvider",port="0")classServiceConsumerPactTest{    @Autowired    privateOkHttpClientokHttpClient;    privateServiceservice;    @BeforeEach    voidbeforeEach(MockServermockServer){        service=newServiceClient(okHttpClient,mockServer.getUrl());    }    @Pact(consumer="serviceConsumer")    publicRequestResponsePactdefaultNameEnglishPact(PactDslWithProviderbuilder){        returnbuilder                .uponReceiving("get message with empty name with en-GB locale interaction")                .method("GET")                .headers("Accept-Language","en-GB")                .path("/message")                .matchQuery("random",".*","16fc8a5f-b9ab-4b26-8049-81a4e7901820")                .willRespondWith()                .status(200)                .body("Hello World!")                .toPact();    }    @Pact(consumer="serviceConsumer")    publicRequestResponsePactcustomNameEnglishPact(PactDslWithProviderbuilder){        returnbuilder                .uponReceiving("get message with a name with en-GB locale interaction")                .method("GET")                .headers("Accept-Language","en-GB")                .path("/message/Java")                .matchQuery("random",".*","16fc8a5f-b9ab-4b26-8049-81a4e7901820")                .willRespondWith()                .status(200)                .body("Hello Java!")                .toPact();    }    @Pact(consumer="serviceConsumer")    publicRequestResponsePactdefaultNameSpanishPact(PactDslWithProviderbuilder){        returnbuilder                .uponReceiving("get message with empty name with es-ES locale interaction")                .method("GET")                .headers("Accept-Language","es-ES")                .path("/message")                .matchQuery("random",".*","16fc8a5f-b9ab-4b26-8049-81a4e7901820")                .willRespondWith()                .status(200)                .body("¡Hola mundo!")                .toPact();    }    @Pact(consumer="serviceConsumer")    publicRequestResponsePactcustomNameSpanishPact(PactDslWithProviderbuilder){        returnbuilder                .uponReceiving("get message with a name with es-ES locale interaction")                .method("GET")                .headers("Accept-Language","es-ES")                .path("/message/Java")                .matchQuery("random",".*","16fc8a5f-b9ab-4b26-8049-81a4e7901820")                .willRespondWith()                .status(200)                .body("¡Hola Java!")                .toPact();    }    @Test    @PactTestFor(pactMethod="defaultNameEnglishPact")    voiddefaultNameEnglish(MockServerms)throwsIOException{        Stringresult=service.message("en-GB",UUID.randomUUID().toString()).execute().body();        assertEquals("Hello World!",result);    }    @Test    @PactTestFor(pactMethod="customNameEnglishPact")    voidcustomNameEnglish()throwsIOException{        Stringresult=service.message("Java","en-GB",UUID.randomUUID().toString()).execute().body();        assertEquals("Hello Java!",result);    }    @Test    @PactTestFor(pactMethod="defaultNameSpanishPact")    voiddefaultNameSpanish(MockServerms)throwsIOException{        Stringresult=service.message("es-ES",UUID.randomUUID().toString()).execute().body();        assertEquals("¡Hola mundo!",result);    }    @Test    @PactTestFor(pactMethod="customNameSpanishPact")    voidcustomNameSpanish()throwsIOException{        Stringresult=service.message("Java","es-ES",UUID.randomUUID().toString()).execute().body();        assertEquals("¡Hola Java!",result);    }}
      ServiceConsumerPactTest.java

      El documento del contrato generado por el consumidor

      Al finalizar las pruebas unitarias del consumidor Pact genera en el directorio build/pact un archivo con las interacciones y sus datos que ha requerido el consumidor en sus pruebas unitarias.

       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
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      
      {    "consumer":{      "name":"serviceConsumer"    },    "interactions":[      {        "description":"get message with a name with en-GB locale interaction",        "request":{          "headers":{            "Accept-Language":"en-GB"          },          "matchingRules":{            "query":{              "random":{                "combine":"AND",                "matchers":[                  {                    "match":"regex",                    "regex":".*"                  }                ]              }            }          },          "method":"GET",          "path":"/message/Java",          "query":{            "random":[              "16fc8a5f-b9ab-4b26-8049-81a4e7901820"            ]          }        },        "response":{          "body":"Hello Java!",          "status":200        }      },      ...    ],    "metadata":{      "pact-jvm":{        "version":"4.2.9"      },      "pactSpecification":{        "version":"3.0.0"      }    },    "provider":{      "name":"serviceProvider"    }}
      serviceConsumer-serviceProvider.json

      Pruebas unitarias del proveedor

      Este archivo es usado para realizar las pruebas unitarias de contrato de la parte proveedora, Pact lee el archivo de interacciones del consumidor y las lanza contra la parte proveedora comprobando los resultados devueltos.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      
      packageio.github.picodotdev.blogbitix.javapact;...@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT,classes=Main.class)@Provider("serviceProvider")@Consumer("serviceConsumer")@PactFolder("build/pacts")classServiceProviderPactTest{    @TestTemplate    @ExtendWith(PactVerificationSpringProvider.class)    voidpactVerificationTestTemplate(PactVerificationContextcontext){        context.verifyInteraction();    }}
      ServiceProviderPactTest.java

      Estas son las dependencias necesarias a incluir en el archivo de construcción Gradle.

       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
      26
      27
      28
      29
      30
      
      plugins{    id'application'}repositories{    mavenCentral()}dependencies{    implementation(platform('org.springframework.boot:spring-boot-dependencies:2.5.3'))    implementation'org.springframework.boot:spring-boot'    implementation'org.springframework.boot:spring-boot-starter-web'    implementation'org.springframework.boot:spring-boot-starter-test'    implementation'com.squareup.retrofit2:retrofit:2.9.0'    implementation'com.squareup.retrofit2:converter-scalars:2.9.0'    testImplementation'org.junit.jupiter:junit-jupiter:5.7.1'    testImplementation'au.com.dius.pact.consumer:junit5:4.2.9'    testImplementation'au.com.dius.pact.provider:junit5spring:4.2.9'}application{    mainClass='io.github.picodotdev.blogbitix.javapact.Main'}tasks.named('test'){    useJUnitPlatform()}
      build.gradle

      Para este ejemplo por sencillez las interacciones del contrato generador por el consumidor es proporcionado a la parte proveedora a través del sistema de archivos. Pact proporciona un servidor Pact Broker donde los consumidores comparten los contratos y de donde los proveedores los obtienen para comprobarlos funcionando como un repositorio de los contratos. Se ofrece la opción de ejecutar Pact Broker mediante contenedores Docker con un archivo de Docker Compose.

      Terminal

      El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
      ./gradlew test

      Viewing all 2698 articles
      Browse latest View live