viernes, 29 de julio de 2022

Crear un proyecto con Sbt y Scala 3


Es muy fácil, mi consejo, instalate sdkman y luego sbt, pero si queres instalarte sbt de una, no hay problema. 

Luego lo que tienes que hacer es : 

> sbt new scala/scala3.g8

Y al toque te pide nombre del proyecto, lo llenas y listo!!

Espero que les sirva. 

jueves, 28 de julio de 2022

Compilar en kotlin


El código fuente de Kotlin normalmente se almacena en archivos con la extensión .kt. El compilador de Kotlin analiza el código fuente y genera archivos .class, al igual que lo hace el compilador de Java. Los archivos .class generados luego se empaquetan y ejecutan utilizando el procedimiento estándar para el tipo de aplicación en la que está trabajando. En el caso más simple, puede usar el comando kotlinc para compilar su código desde la línea de comandos y usar el comando java para ejecutar su código:


kotlinc <source file or directory> -include-runtime -d <jar name>

java -jar <jar name>


El código compilado con el compilador de Kotlin depende de la biblioteca de runtime de Kotlin. Contiene las definiciones de las propias clases de biblioteca estándar de Kotlin y las extensiones que Kotlin agrega a las API estándar de Java. La biblioteca de runtime debe distribuirse con su aplicación.

En la mayoría de los casos de la vida real, usará un sistema de compilación como Maven, Gradle o Ant para compilar su código. Kotlin es compatible con todos esos sistemas de compilación. Todos esos sistemas de compilación también admiten proyectos de lenguaje mixto que combinan Kotlin y Java en la misma base de código. Además, Maven y Gradle se encargan de incluir la biblioteca en tiempo de ejecución de Kotlin como una dependencia de tu aplicación.

miércoles, 27 de julio de 2022

Filosofía de Kotlin


Cuando hablamos de Kotlin, es un lenguaje pragmático, conciso y seguro con un enfoque en la interoperabilidad. ¿Qué significan exactamente con cada una de esas palabras? 

Pragmático: Kotlin es un lenguaje práctico diseñado para resolver problemas del mundo real. Su diseño se basa en muchos años de experiencia en la industria creando sistemas a gran escala, y sus funciones se eligen para abordar los casos de uso que encuentran muchos desarrolladores de software. Además, los desarrolladores tanto dentro de JetBrains como en la comunidad han estado usando las primeras versiones de Kotlin durante varios años y sus comentarios han dado forma a la versión lanzada del lenguaje. Kotlin puede ayudar a resolver problemas en proyectos reales.

Kotlin tampoco es un lenguaje de investigación. No estamos tratando de avanzar en el estado del arte en el diseño de lenguajes de programación y explorar ideas innovadoras en informática. En su lugar, siempre que sea posible, confiamos en funciones y soluciones que ya han aparecido en otros lenguajes de programación y han demostrado ser exitosas. Esto reduce la complejidad del lenguaje y hace que sea más fácil de aprender al permitirle confiar en conceptos familiares.

 Además, Kotlin no impone el uso de ningún estilo o paradigma de programación en particular. A medida que comienza a estudiar el lenguaje, puede usar el estilo y las técnicas que le resultan familiares de su experiencia con Java. Más adelante, descubrirá gradualmente las funciones más potentes de Kotlin y aprenderá a aplicarlas en su propio código, para hacerlo más conciso e idiomático.

Otro aspecto del pragmatismo de Kotlin es su enfoque en las herramientas. Un entorno de desarrollo inteligente es tan esencial para la productividad de un desarrollador como un buen lenguaje; y debido a eso, tratar el soporte IDE como una ocurrencia tardía no es una opción. En el caso de Kotlin, el complemento IntelliJ IDEA se desarrolló al unísono con el compilador, y las características del lenguaje siempre se diseñaron teniendo en cuenta las herramientas.

La compatibilidad con IDE también juega un papel importante a la hora de ayudarte a descubrir las características de Kotlin. En muchos casos, las herramientas detectarán automáticamente patrones de código comunes que se pueden reemplazar por construcciones más concisas y se ofrecerán a corregir el código por usted. Al estudiar las características del lenguaje utilizadas por las correcciones automáticas, también puede aprender a aplicar esas características en su propio código.

Conciso: Es bien sabido que los desarrolladores dedican más tiempo a leer código existente que a escribir código nuevo. Imagina que eres parte de un equipo que está desarrollando un gran proyecto y necesitas agregar una nueva función o corregir un error. ¿Cuáles son tus primeros pasos? Busca la sección exacta del código que necesita cambiar y solo entonces implementa una corrección. Lees mucho código para saber lo que tienes que hacer. Este código puede haber sido escrito recientemente por sus colegas, por alguien que ya no trabaja en el proyecto, o por usted, pero hace mucho tiempo. Solo después de comprender el código que lo rodea, puede realizar las modificaciones necesarias.

Cuanto más simple y conciso sea el código, más rápido entenderá lo que está pasando. Por supuesto, el buen diseño y los nombres expresivos juegan un papel importante aquí. Pero la elección del lenguaje y su concisión también son importantes. El lenguaje es conciso si su sintaxis expresa claramente la intención del código que lee y no lo oscurece con el texto estándar requerido para especificar cómo se logra la intención.

 En Kotlin, todo el código que escribe tiene un significado y no esta ahí solo para satisfacer los requisitos de la estructura del código. Gran parte del código estándar de Java, como getters, setters y la lógica para asignar parámetros de constructor a los campos, está implícito en Kotlin y no abarrota el código fuente.

Otra razón por la que el código puede ser innecesariamente largo es tener que escribir código explícito para realizar tareas comunes, como ubicar un elemento en una colección. Al igual que muchos otros lenguajes modernos, Kotlin tiene una rica biblioteca estándar que le permite reemplazar estas secciones largas y repetitivas de código con llamadas a métodos de biblioteca. La compatibilidad de Kotlin con lambdas facilita el paso de pequeños bloques de código a funciones de biblioteca. Esto le permite encapsular todas las partes comunes en la biblioteca y mantener solo la porción única y específica de la tarea en el código de usuario.

Al mismo tiempo, Kotlin no intenta reducir el código fuente al menor número de caracteres posible. Por ejemplo, aunque Kotlin admite la sobrecarga de operadores, los usuarios no pueden definir sus propios operadores. Por lo tanto, los desarrolladores de bibliotecas no pueden reemplazar los nombres de los métodos con secuencias de puntuación crípticas. Las palabras suelen ser más fáciles de leer que la puntuación y es más fácil encontrar documentación.

Un código más conciso requiere menos tiempo para escribir y, lo que es más importante, menos tiempo para leer. Esto mejora la productividad y permite hacer las cosas más rápido.

Seguro: En general, cuando hablamos de un lenguaje de programación como seguro, nos referimos a que su diseño previene ciertos tipos de errores en un programa. Por supuesto, esto no es un absoluto igualdad; ningún lenguaje previene todos los posibles errores. Además, la prevención de errores generalmente tiene un costo. Debe proporcionar al compilador más información sobre la operación prevista del programa, de modo que el compilador pueda verificar que la información coincida con lo que hace el programa. Por eso, siempre hay una compensación entre el nivel de seguridad que se obtiene y la pérdida de productividad necesaria para realizar anotaciones más detalladas.

Con Kotlin, se logra un mayor nivel de seguridad que en Java, con un costo total menor. La ejecución en la JVM ya proporciona muchas garantías de seguridad: por ejemplo, seguridad de la memoria, prevención de desbordamientos de búfer y otros problemas causados ​​por el uso incorrecto de la memoria asignada dinámicamente. Como lenguaje tipificado estáticamente en JVM, Kotlin también garantiza la seguridad de tipos de sus aplicaciones. Esto tiene un costo menor que con Java: no tiene que especificar todas las declaraciones de tipos, porque en muchos casos el compilador infiere los tipos automáticamente.

Kotlin también va más allá, lo que significa que se pueden evitar más errores mediante comprobaciones en tiempo de compilación en lugar de fallar en tiempo de ejecución. Lo más importante es que Kotlin se esfuerza por eliminar NullPointerException de su programa. El sistema de tipos de Kotlin realiza un seguimiento de los valores que pueden y no pueden ser nulos y prohíbe las operaciones que pueden conducir a una excepción NullPointer en tiempo de ejecución. El costo adicional requerido para esto es mínimo: marcar un tipo como anulable requiere solo un carácter, un signo de interrogación al final:

val s: String? = null

val s2: String = ""

Además, Kotlin proporciona muchas formas convenientes de manejar datos anulables. Esto es de gran ayuda para eliminar los bloqueos de aplicaciones.

Otro tipo de excepción que Kotlin ayuda a evitar es ClassCastException. Ocurre cuando conviertes un objeto en un tipo sin verificar primero que tenga el tipo correcto. En Java, los desarrolladores a menudo omiten la verificación, porque el nombre del tipo debe repetirse en la verificación y en el siguiente lanzamiento. En Kotlin, por otro lado, la verificación y el elenco se combinan en una sola operación: una vez que hayas verificado el tipo, puedes referirte a los miembros de ese tipo sin ningún casteo adicional. Por lo tanto, no hay motivo para omitir la verificación y no hay posibilidad de cometer un error. Así es como funciona esto:

if (value is String) println(value.toUpperCase())

Interoperable: Con respecto a la interoperabilidad, su primera preocupación probablemente sea: "¿Puedo usar mis bibliotecas existentes?" Con Kotlin, la respuesta es: "Sí, absolutamente". Independientemente del tipo de API que la biblioteca requiera que uses, puedes trabajar con ellas desde Kotlin. Puede llamar a métodos de Java, extender clases de Java e implementar interfaces, aplicar anotaciones de Java a sus clases de Kotlin, etc.

 A diferencia de otros lenguajes JVM, Kotlin va aún más allá con la interoperabilidad, lo que hace que llamar al código Kotlin desde Java también sea sencillo. No se requieren trucos: las clases y los métodos de Kotlin se pueden llamar exactamente como las clases y los métodos de Java normales. Esto le brinda la máxima flexibilidad para combinar código Java y Kotlin en cualquier parte de su proyecto. Cuando comience a adoptar Kotlin en su proyecto de Java, puede ejecutar el convertidor de Java a Kotlin en cualquier clase de su base de código, y el resto del código seguirá compilando y funcionando sin modificaciones. Esto funciona independientemente del rol de la clase que haya convertido.

Otra área en la que Kotlin se centra en la interoperabilidad es el uso de las bibliotecas Java existentes en la mayor medida posible. Por ejemplo, Kotlin no tiene su propia biblioteca de colecciones. Se basa completamente en las clases de la biblioteca estándar de Java y las amplía con funciones adicionales para un uso más conveniente en Kotlin. (Veremos el mecanismo para esto con más detalle en la sección 3.3). Esto significa que nunca necesitará envolver o convertir objetos cuando llame a las API de Java desde Kotlin, o viceversa. Toda la riqueza de API proporcionada por Kotlin viene sin costo en tiempo de ejecución.

 Las herramientas de Kotlin también brindan soporte completo para proyectos en varios lenguajes. Puede compilar una combinación arbitraria de archivos fuente de Java y Kotlin, independientemente de cómo dependan entre sí. Las funciones del IDE también funcionan en todos los lenguajes, lo que le permite:

  • Navegar libremente entre los archivos fuente de Java y Kotlin
  • Depurar proyectos de lenguaje mixto y pasar entre código escrito en diferentes lenguajes
  • Refactorizar sus métodos Java y actualice correctamente su uso en el código Kotlin, y viceversa. 



lunes, 25 de julio de 2022

Interceptor de Retrofit

                                                                                                                                                                                                                                                                        

Supongamos que tenemos que enviar en todas las llamadas a web services, un valor o un token o cualquier cosa. 

Lo que podemos hacer con retrofit es un interceptor, este lo declaramos una vez y para todas las llamadas enviarán los valores. Veamos un ejemplo: 

@Component

class RetrofitClient {


    @Value("\${service.url}")

    private lateinit var baseUrl: String


    private lateinit var retrofit: Retrofit


    private lateinit var objectMapper: ObjectMapper


    @PostConstruct

    fun init() {

        objectMapper = ObjectMapper()

            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

            .registerModule(KotlinModule())


        val client = OkHttpClient.Builder().addInterceptor(EjemploInterceptor()).build()


        retrofit = Retrofit.Builder()

            .baseUrl(baseUrl)

            .client(client)

            .addConverterFactory(JacksonConverterFactory.create(objectMapper))

            .build()

    }


    fun <T> buildService(service: Class<T>): T {

        return retrofit.create(service)

    }


}


class EjemploInterceptor : Interceptor {


    companion object {

        private const val EJEMPLO = "ejemplo"

    }


    override fun intercept(chain: Interceptor.Chain): Response = chain.run {

        proceed(

            request()

                .newBuilder()

                .addHeader(EJEMPLO, "valor a agregar")

                .build()

        )

    }

}


En este ejemplo en todas las llamadas enviarán el valor agregado en el head. Y listo! 

jueves, 21 de julio de 2022

Mónadas en Cats parte 18

El log en un Writer se conserva cuando hacemos map or flatMap sobre él. flatMap agrega los logs del Writer de origen y el resultado de la función de secuenciación del usuario. Por esta razón, es una buena práctica utilizar un tipo de registro que tenga operaciones de adición y concatenación eficientes, como un Vector:

val writer1 = for {

    a <- 10.pure[Logged]

    _ <- Vector("a", "b", "c").tell

    b <- 32.writer(Vector("x", "y", "z"))

} yield a + b

// writer1: cats.data.WriterT[cats.package.Id, Vector[String], Int] =WriterT(

//(Vector("a", "b", "c", "x", "y", "z"), 42)

// )

writer1.run

// res3: (Vector[String], Int) = (Vector("a", "b", "c", "x", "y", "z") , 42)


Además de transformar el resultado con map y flatMap, podemos transformar el log en un Writer con el método mapWritten:


val writer2 = writer1.mapWritten(_.map(_.toUpperCase))

// writer2: cats.data.WriterT[cats.package.Id, Vector[String], Int] = WriterT(

// (Vector("A", "B", "C", "X", "Y", "Z"), 42)

// )

writer2.run

// res4: (Vector[String], Int) = (Vector("A", "B", "C", "X", "Y", "Z") , 42)


Podemos transformar tanto el log como el resultado simultáneamente usando bimap o mapBoth. bimap toma dos parámetros de función, uno para el registro y otro para el resultado. mapBoth toma una sola función que acepta dos parámetros:


val writer3 = writer1.bimap(

    log => log.map(_.toUpperCase),

    res => res * 100

)

// writer3: cats.data.WriterT[cats.package.Id, Vector[String], Int] = WriterT(

//(Vector("A", "B", "C", "X", "Y", "Z"), 4200)

// )

writer3.run

// res5: (Vector[String], Int) = (Vector("A", "B", "C", "X", "Y", "Z"), 4200)

val writer4 = writer1.mapBoth { (log, res) =>

    val log2 = log.map(_ + "!")

    val res2 = res * 1000

    (log2, res2)

}

// writer4: cats.data.WriterT[cats.package.Id, Vector[String], Int] =WriterT(

//(Vector("a!", "b!", "c!", "x!", "y!", "z!"), 42000)

// )

writer4.run

// res6: (Vector[String], Int) = (

//Vector("a!", "b!", "c!", "x!", "y!", "z!"),

//42000

// )


Finalmente, podemos borrar el registro con el método de reinicio e intercambiar el registro y el resultado con el método de intercambio:


val writer5 = writer1.reset

// writer5: cats.data.WriterT[cats.package.Id, Vector[String], Int] = WriterT(

//(Vector(), 42)

// )

writer5.run

// res7: (Vector[String], Int) = (Vector(), 42)

val writer6 = writer1.swap

// writer6: cats.data.WriterT[cats.package.Id, Int, Vector[String]] = WriterT(

//(42, Vector("a", "b", "c", "x", "y", "z"))

// )

writer6.run

// res8: (Int, Vector[String]) = (42, Vector("a", "b", "c", "x", "y","z"))

lunes, 18 de julio de 2022

Cualidades clave de Kotlin


Estáticamente tipado
: al igual que Java, Kotlin es un lenguaje de programación con tipos estáticos. Esto significa que el tipo de cada expresión en un programa se conoce en el momento de la compilación, y el compilador puede validar que los métodos y campos a los que intenta acceder existen en los objetos que está utilizando. Esto contrasta con los lenguajes de programación tipificados dinámicamente, que están presentes en la JVM, entre otros, Groovy, Clojure y JRuby. Esos lenguajes le permiten definir variables y funciones que pueden almacenar o devolver datos de cualquier tipo y resolver las referencias de métodos y campos en tiempo de ejecución. Esto permite un código más corto y una mayor flexibilidad en la creación de estructuras de datos. Pero la desventaja es que los problemas como los nombres mal escritos no se pueden detectar durante la compilación y pueden provocar errores de tiempo de ejecución.

Por otro lado, a diferencia de Java, Kotlin no requiere que especifiques el tipo de cada variable explícitamente en tu código fuente. En muchos casos, el tipo de una variable se puede determinar automáticamente a partir del contexto, lo que le permite omitir la declaración de tipo. Aquí está el ejemplo más simple posible de esto:

val x = 1

Estás declarando una variable y, como se inicializa con un valor entero, Kotlin determina automáticamente que su tipo es Int. La capacidad del compilador para determinar tipos a partir del contexto se denomina inferencia de tipos.

Los siguientes son algunos de los beneficios del tipado estático:

  • Rendimiento: llamar a los métodos es más rápido porque no es necesario averiguar en tiempo de ejecución qué método debe llamarse.
  • Confiabilidad: el compilador verifica la corrección del programa, por lo que hay menos posibilidades de fallas durante el tiempo de ejecución.
  • Capacidad de mantenimiento: trabajar con código desconocido es más fácil porque puede ver con qué tipo de objetos está trabajando el código.
  • Compatibilidad con herramientas: la escritura estática permite refactorizaciones confiables, finalización de código precisa y otras características de IDE.

Gracias a la compatibilidad de Kotlin con la inferencia de tipos, la mayor parte de la verbosidad adicional asociada con la escritura estática desaparece porque no es necesario declarar los tipos explícitamente.

Si observa los detalles del sistema de tipos de Kotlin, encontrará muchos conceptos familiares. Las clases, las interfaces y los genéricos funcionan de manera muy similar a Java, por lo que la mayor parte de su conocimiento de Java debería transferirse fácilmente a Kotlin. Sin embargo, algunas cosas son nuevas.

El más importante de ellos es el soporte de Kotlin para tipos anulables, lo que le permite escribir programas más confiables al detectar posibles excepciones de puntero nulo en el momento de la compilación. 

Otra cosa nueva en el sistema de tipos de Kotlin es su soporte para tipos de funciones. Para ver de qué se trata, veamos las ideas principales de la programación funcional y veamos cómo se admite en Kotlin.

Programación funcional y orientada a objetos: Como desarrollador de Java, sin duda estamos familiarizado con los conceptos básicos de la programación orientada a objetos, pero la programación funcional puede ser nueva para desarrolladores Java. Los conceptos clave de la programación funcional son los siguientes:

  • Funciones de primera clase: Puede almacenar funciones en variables, pasarlos como parámetros o devolverlos desde otras funciones.
  • Inmutabilidad: trabaja con objetos inmutables, lo que garantiza que su estado no puede cambiar después de su creación.
  • Sin efectos secundarios: utiliza funciones puras que devuelven el mismo resultado dadas las mismas entradas y no modifican el estado de otros objetos ni interactúan con el mundo exterior.

¿Qué beneficios puede obtener al escribir código en el estilo funcional? Primero, concisión. El código funcional puede ser más elegante y sucinto en comparación con su contraparte imperativa, porque trabajar con funciones como valores le brinda mucho más poder de abstracción, lo que le permite evitar la duplicación en su código.

Imagine que tiene dos fragmentos de código similares que implementan una tarea similar (por ejemplo, buscar un elemento coincidente en una colección) pero difieren en los detalles (cómo se detecta el elemento coincidente). Puede extraer fácilmente la parte común de la lógica en una función y pasar las partes diferentes como argumentos. Esos argumentos son en sí mismos funciones, pero puede expresarlos usando una sintaxis concisa para funciones anónimas llamadas expresiones lambda:

fun findAlice() = findPerson { it.name == "Alice" }

fun findBob() = findPerson { it.name == "Bob" }

El segundo beneficio del código funcional es el subprocesamiento múltiple seguro. Una de las mayores fuentes de errores en los programas de subprocesos múltiples es la modificación de los mismos datos de varios subprocesos sin la sincronización adecuada. Si usa estructuras de datos inmutables y funciones puras, puede estar seguro de que tales modificaciones inseguras no ocurrirán y no necesita idear esquemas de sincronización complicados.

Finalmente, la programación funcional significa pruebas más fáciles. Las funciones sin efectos secundarios se pueden probar de forma aislada sin necesidad de una gran cantidad de código de configuración para construir todo el entorno del que dependen.

En términos generales, el estilo funcional se puede usar con cualquier lenguaje de programación, incluido Java, y muchas partes del mismo se recomiendan como un buen estilo de programación. Pero no todos los lenguajes brindan el soporte sintáctico y de biblioteca necesario para usarlo sin esfuerzo; por ejemplo, este soporte faltaba en su mayoría en las versiones de Java anteriores a Java 8. Kotlin tiene un amplio conjunto de características para admitir la programación funcional desde el principio. Estos incluyen lo siguiente:

  • Tipos de funciones, que permiten que las funciones reciban otras funciones como parámetros o devuelvan otras funciones
  • Expresiones lambda, que le permiten pasar bloques de código con un mínimo de repeticiones
  • Clases de datos, que proporcionan una sintaxis concisa para crear objetos de valor inmutable
  • Un amplio conjunto de API en la biblioteca estándar para trabajar con objetos y colecciones en el estilo funcional

Kotlin le permite programar en el estilo funcional pero no lo impone. Cuando lo necesite, puede trabajar con datos mutables y escribir funciones que tienen efectos secundarios sin tener que pasar por obstáculos adicionales. Y, por supuesto, trabajar con marcos que se basan en interfaces y jerarquías de clases es tan fácil como con Java. Al escribir código en Kotlin, puede combinar los enfoques funcional y orientado a objetos y utilizar las herramientas más apropiadas para el problema que está resolviendo.

Gratuito y open source: El lenguaje Kotlin, incluido el compilador, las bibliotecas y todas las herramientas relacionadas, es completamente de código abierto y de uso gratuito para cualquier propósito. Está disponible bajo la licencia Apache 2; el desarrollo ocurre abiertamente en GitHub (http://github.com/jetbrains/kotlin), y las contribuciones de la comunidad son bienvenidas. También puede elegir entre tres IDE de código abierto para desarrollar sus aplicaciones de Kotlin: IntelliJ IDEA Community Edition, Android Studio y Eclipse son totalmente compatibles. (Por supuesto, IntelliJ IDEA Ultimate también funciona).

Apis en diferentes lenguajes y frameworks en la plataforma Java


Estoy haciendo una aplicación con n microservicios en n tipos de tecnologías y lenguajes todos en la plataforma Java. 

Los de n tipos de tecnologías es mentiroso solo 2 framework use por ahora Spring boot y micronaut y a decir verdad use un 80% de spring boot pero quiero escribir un post tipo draft, por las dudas si me canso y nunca termino. 

Por ende, vamos a ver como me fue con Spring boot y diferentes lenguajes y luego hablo de micronaut. 

Java: Bueno, esta es la tupla más utilizada y creo que ustedes saben que anda muy bien. No sé si puedo agregar algo más que no sepan. 

Kotlin: Me encanto, por varias razones me parece mejor que java. Kotlin es un lenguaje, muy bueno y muy compatible con java lo que lo hace una opción super fácil de usar y no se sufre nada con la compatibilidad con spring boot. 

Groovy: Igual que Java, no encuentro una buena razón para elegir este lenguaje, esta bueno, pero el tipado dinamico, no ofrece mayor ventaja y si lo usamos con tipando estático, es similar a java o kotlin. 

Scala: Un infierno de incompatibilidad, no le recomiendo utilizar spring boot con scala, tenes que estar siempre cambiando tipos, de tipos de Scala a Java y de Java a Scala. No es una opción práctica...

Clojure: Otro infierno, no solo por los tipos, sino tambien por la generación de clases con anotaciones. Muy trabajoso, al final cuando te funciona una Api, no te acordas para que la querías. 

Micronaut lo utilice muy poco con Groovy y Java y me fue muy bien, casi casi puedo decir que me gusta más que spring boot. Y ahora sigo con Micronaut y Quarkus. 

sábado, 16 de julio de 2022

Por qué usar Kotlin?


El objetivo principal de Kotlin es proporcionar una alternativa a Java más concisa, más productiva y más segura que sea adecuada en todos los contextos en los que se usa Java en la actualidad. Java es un lenguaje extremadamente popular y se usa en una amplia variedad de entornos, desde tarjetas inteligentes (tecnología de tarjeta Java) hasta los centros de datos más grandes administrados por Google, Twitter, LinkedIn y otras empresas. En la mayoría de estos lugares, el uso de Kotlin puede ayudar a los desarrolladores a lograr sus objetivos con menos código y menos molestias en el camino.

Las áreas más comunes para usar Kotlin son:

  • Creación de código del lado del servidor (normalmente, backends de aplicaciones web)
  • Creación de aplicaciones móviles que se ejecutan en dispositivos Android

Pero Kotlin también funciona en otros contextos. Por ejemplo, puede usar Intel Multi-OS Engine (https://software.intel.com/en-us/multi-os-engine) para ejecutar código Kotlin en dispositivos iOS. Para crear aplicaciones de escritorio, puede usar Kotlin junto con TornadoFX (https://github.com/edvin/tornadofx) y JavaFX.1

Además de Java, Kotlin se puede compilar en JavaScript, lo que le permite ejecutar el código de Kotlin en el navegador.

El objetivo de Kotlin es bastante amplio. Kotlin no se enfoca en un solo dominio de problema ni aborda un solo tipo de desafío que enfrentan los desarrolladores de software en la actualidad. En su lugar, proporciona mejoras de productividad generales para todas las tareas que surgen durante el proceso de desarrollo. Y brinda un excelente nivel de integración con bibliotecas que admiten dominios específicos o paradigmas de programación. 

viernes, 15 de julio de 2022

[Whitepaper] Speed vs. Security: Protecting Modern Apps and APIs at the Pace of Modern Business

 

WHITEPAPER

https://interact.f5.com/rs/653-SMC-783/images/WP - Speed vs. Security Protecting Modern Apps and APIs at the Pace of Modern Business - 760x284.png

Hi Emanuel,

Customers, partners, and employees do not just demand the most from your technology-driven services, they expect it. DevOps, microservices, and containers can all help to deliver this much sought-after application agility. In this whitepaper you will learn that by integrating security early into the development process – shifting left – your organization can achieve both application and API agility and security, driving innovation while staying ahead of the competition.

In this whitepaper you will learn:

  • Why traditional security approaches are no longer suitable for your modern apps and APIs
  • Methods hackers use to target web applications, with 40% of attacks coming through APIs
  • How to achieve application speed and agility to ensure your business provides customers with the best possible experience
  • About integrating security early into the development lifecycle and automating policy so your business can remove bottlenecks, support shift left DevOps initiatives, and accelerate app velocity

martes, 12 de julio de 2022

Mónadas en Cats parte 17

cats.data.Writer es una mónada que nos permite llevar un registro junto con un cálculo. Podemos usarlo para registrar mensajes, errores o datos adicionales sobre un cálculo y extraer el registro junto con el resultado final.

Un uso común para Writers es registrar secuencias de pasos en cálculos de subprocesos múltiples donde las técnicas de registro imperativas estándar pueden generar mensajes intercalados de diferentes contextos. Con Writer, el registro para el cálculo está vinculado al resultado, por lo que podemos ejecutar cálculos simultáneos sin mezclar registros.

Un Writer[W, A] tiene dos valores: un registro de tipo W y un resultado de tipo A. Podemos crear un Writer a partir de valores de cada tipo de la siguiente manera:


import cats.data.Writer

import cats.instances.vector._ // for Monoid

Writer(Vector("It was the best of times","it was the worst of times"), 1859)

// res0: cats.data.WriterT[cats.package.Id, Vector[String], Int] = WriterT(

// (Vector("It was the best of times", "it was the worst of times"), 1859)

// )


Tenga en cuenta que el tipo informado en la consola es en realidad WriterT[Id, Vector[String], Int] en lugar de Writer[Vector[String], Int] como cabría esperar. En el espíritu de la reutilización de código, Cats implementa Writer en términos de otro tipo, WriterT. WriterT es un ejemplo de un nuevo concepto llamado transformador de mónadas, que trataremos más adelante

Tratemos de ignorar este detalle por ahora. Writer es un alias de tipo para WriterT, por lo que podemos leer tipos como WriterT[Id, W, A] como Writer[W, A]:

type Writer[W, A] = WriterT[Id, W, A]

Para mayor comodidad, Cats proporciona una forma de crear Writers especificando solo el registro o el resultado. Si solo tenemos un resultado, podemos usar la sintaxis pure estándar. Para hacer esto, debemos tener un Monoid[W] en el alcance para que Cats sepa cómo producir un registro vacío:

import cats.instances.vector._ // for Monoid

import cats.syntax.applicative._ // for pure

type Logged[A] = Writer[Vector[String], A]

123.pure[Logged]

// res1: Logged[Int] = WriterT((Vector(), 123))

Si tenemos un registro y ningún resultado, podemos crear un Writer[Unit] usando la sintaxis tell de cats.syntax.writer:

import cats.syntax.writer._ // for tell

Vector("msg1", "msg2", "msg3").tell

// res2: Writer[Vector[String], Unit] = WriterT(

// (Vector("msg1", "msg2", "msg3"), ())

// )

Si tenemos un resultado y un registro, podemos usar Writer.apply o podemos usar la sintaxis de Writer de cats.syntax.writer:

import cats.syntax.writer._ // for writer

val a = Writer(Vector("msg1", "msg2", "msg3"), 123)

// a: cats.data.WriterT[cats.package.Id, Vector[String], Int] = WriterT(

// (Vector("msg1", "msg2", "msg3"), 123)

// )

val b = 123.writer(Vector("msg1", "msg2", "msg3"))

// b: Writer[Vector[String], Int] = WriterT(

// (Vector("msg1", "msg2", "msg3"), 123)

// )

Podemos extraer el resultado y el registro de un Writer utilizando los métodos de valor y escrito respectivamente:

val aResult: Int = a.value

// aResult: Int = 123

val aLog: Vector[String] =a.written

// aLog: Vector[String] = Vector("msg1", "msg2", "msg3")

Podemos extraer ambos valores al mismo tiempo usando el método de ejecución:

val (log, result) = b.run

// log: Vector[String] = Vector("msg1", "msg2", "msg3")

// result: Int = 123

sábado, 9 de julio de 2022

Hacer un test de integración de un dao que guarda en Redis con Testcontainers y Spring boot


Me quedo relargo el titulo, pero la idea sería:

Tenemos una aplicación de spring boot y spring data que guarda información en Redis con un Dao y queremos probar este dao, ¿como lo hacemos?. Podemos usar testcontainers. 

Testcontainers es un framework que levanta una imagen de docker para que diferentes tecnologías esten accesibles para nuestros tests. 

Para utilizarlo debemos agregar esta dependencia en gradle o maven :


testImplementation "org.testcontainers:testcontainers:1.17.2"


Y en este ejemplo vamos a utilizar kotlin, pero funciona tambien con Java: 


@RunWith(SpringJUnit4ClassRunner::class)

@SpringBootTest(classes = [Application::class])

class EjemploRepositoryTest {


    @Autowired

    lateinit var ejemploRepository: EjemploRepository


    companion object {

        

        init {

            val redis : GenericContainer<Nothing>  = GenericContainer<Nothing>(DockerImageName.parse("redis:5.0.3-alpine"))

                .withExposedPorts(6379)


            redis.start()


            System.setProperty("redis.host", "${redis.host}")

            System.setProperty("redis.port", "${redis.firstMappedPort}")

        }

    }


    @Test

    fun `save and find all ejemplos`() {

        val count = ejemploRepository.findAll().count()

        val ejemplo = crearUnEjemplo() //este metodo te crea un ejemplo :D


        ejemploRepository.save(ejemplo)

        val ejemplos = ejemplo.findAll()


        Assert.assertNotNull(ejemplos)

        Assert.assertEquals(count + 1, ejemplos.count())

    }


}

En el código anterior falta el metodo crear ejemplo que va a crear el objeto para probar, no lo agrego porque no lo veo necesario. Por supuesto, el objeto ejemplo debe seguir con las notaciones de Spring data. 

Y listo! 

jueves, 7 de julio de 2022

Mónadas en Cats parte 16

Una propiedad útil de Eval es que sus métodos map y flatMap son trampolines. Esto significa que podemos anidar llamadas a map y flatMap arbitrariamente sin consumir  llamadas de la pila. Llamamos a esta propiedad "seguridad de pila". Por ejemplo, considere esta función para calcular factoriales:


def factorial(n: BigInt): BigInt = if(n == 1) n else n * factorial(n - 1)


Es relativamente fácil que la pila desborde:

factorial(50000)

// java.lang.StackOverflowError

//

...

Podemos reescribir el método usando Eval para hacerlo seguro:

def factorial(n: BigInt): Eval[BigInt] = if(n == 1) {

        Eval.now(n)

    } else {

        factorial(n - 1).map(_ * n)

    }

factorial(50000).value

// java.lang.StackOverflowError

//

...

¡Ups! Eso no funcionó, ¡nuestra pila aún explotó! Esto se debe a que aún estamos realizando todas las llamadas recursivas a factorial antes de comenzar a trabajar con el método de mapa de Eval. Podemos solucionar esto usando Eval.defer, que toma una instancia existente de Eval y difiere su evaluación. El método diferido es un trampolín como map y flatMap, por lo que podemos usarlo como una forma rápida de hacer que una pila de operaciones existente sea segura:


def factorial(n: BigInt): Eval[BigInt] = if(n == 1) {

        Eval.now(n)

    } else {

        Eval.defer(factorial(n - 1).map(_ * n))

    }

factorial(50000).value

// res: A very big value



martes, 5 de julio de 2022

¿Qué causó el aumento de la popularidad de la programación funcional?



Cuando comencé a dar clases de paradigmas de programación, la programación funcional solo se estudiaba con fines académicos o para la investigación, era poco usado en la industria. Pero esto ha cambiado. La industria ve con buenos ojos a este paradigma y está presente en todos los lenguajes de programación modernos. Para analizar este cambio debemos entender que es la programación funcional. 

 ¿Qué es la programación funcional? 

 Las principales características distintivas del desarrollo de software con Programación funcional son: 

  •     funciones puras; las cuales pueden componer nuevas funciones 
  •     evitar el estado compartido, datos mutables y efectos secundarios; 
  •     La prevalencia de un enfoque declarativo más que imperativo. 

Analicemos estos puntos más en detalle:  

funciones puras: Las funciones puras son funciones deterministas sin efectos secundarios. Una función determinista significa que, para el mismo conjunto de valores de entrada, devuelve el mismo resultado. Las propiedades de tales funciones son muy importantes: por ejemplo, las funciones puras tienen transparencia referencial, se puede reemplazar una llamada de función con su valor final sin cambiar el valor del programa. Y la composición de funciones se refiere al proceso de combinar dos o más funciones para crear una nueva función o realizar cálculos. 

evitar el estado compartido: El principal problema con los estados compartidos es que, para comprender los efectos de una función, debe conocer el historial completo de cada variable compartida que utiliza la función. Por lo tanto, la programación funcional evita estados compartidos, confiando en cambio en estructuras de datos inmutables y computación sin procesar para extraer nuevos datos de los existentes. Otro matiz que surge cuando se trabaja con estados compartidos es que cambiar el orden de las llamadas a funciones puede provocar una avalancha de errores. En consecuencia, al evitar estados compartidos, también evita este problema. 

Subyacente a toda la programación funcional está la inmutabilidad. Los objetos inmutables no se pueden cambiar en absoluto. Esto se logra mediante la congelación profunda de las variables. 

Con estas herramientas se evitan los efectos secundarios, los cuales significan que, además de devolver un valor, la función también interactúa con el estado mutable externo. ¿Por qué Programación funcional los evita? Porque de esta manera los efectos del programa son mucho más fáciles de entender y probar. Haskell, por ejemplo, usa mónadas para aislar los efectos secundarios de las funciones puras. 

enfoque declarativo: El punto es que el enfoque imperativo funciona según el principio de control de flujo y responde a la pregunta "cómo hacerlo". El enfoque declarativo describe el flujo de datos y responde a la pregunta "qué hacer". Además, el código imperativo a menudo se basa en instrucciones (operadores), mientras que el código declarativo se basa en expresiones. 

Ventajas 

Entonces, descubrimos qué es la programación funcional y qué necesitamos saber al respecto. Ahora veamos las ventajas de utilizarlo.   

Uno de los beneficios más obvios de la programación funcional son las abstracciones de alto nivel que ocultan muchos de los detalles de las operaciones de rutina, como la iteración. Debido a esto, el código es más corto y, como resultado, garantiza menos errores que se pueden cometer. 

Además, el Programación funcional contiene menos primitivas de lenguaje. Las clases simplemente no se usan: en lugar de crear una descripción única de un objeto con operaciones en forma de métodos, la programación funcional utiliza varias primitivas básicas del lenguaje que están bien optimizadas internamente. 

Además, la programación funcional permite al desarrollador acercar el lenguaje al problema, en lugar de viceversa, todo a expensas de las estructuras flexibles y la flexibilidad del lenguaje. Además, Programación funcional ofrece a los desarrolladores nuevas herramientas para resolver problemas complejos . 

De hecho, es demasiado largo para enumerar todas las ventajas de la programación funcional; realmente hay muchas de ellas. Puedo decir esto: trabajar con lenguajes funcionales proporciona una escritura de código precisa y rápida, facilita las pruebas y la depuración, los programas son de nivel superior y las firmas de funciones son más informativas. 

La programación funcional permite escribir código más conciso y predecible, y es más fácil de probar (aunque aprender no es fácil). 

Popularidad 

En el mundo de TI, nada sucede porque sí. Una cosa se aferra a la otra, y ahora todas las tendencias más actuales están interconectadas. 

Si recordamos las tendencias más sensacionales de 2016-2017, estas, por supuesto, serán AI, IoT, Big Data y Blockchain. Estaban y están en boca de todos, todos conocen su potencial y sus características claves. Y son algunas de estas tendencias las que han catalizado la creciente popularidad de la programación funcional entre los desarrolladores. 

Actualmente, el problema del procesamiento paralelo y el trabajo con grandes flujos de datos se ha incrementado notablemente. Y, paralelizando el procesamiento de estos datos, podemos obtener el resultado deseado en menor tiempo que con un procesamiento secuencial. Además, la informática descentralizada (distribuida): blockchains y otras, que, en esencia, son un mecanismo bastante complejo. Y para tales cálculos, el código funcional es más adecuado debido a todos los principios de la programación funcional (como las funciones puras, por ejemplo). El uso de todas las técnicas básicas facilita la ejecución y el mantenimiento de código paralelo y distribuido. 

Además, la programación funcional no solo se usaba para resolver problemas específicos, dado el incremento de su popularidad, ahora incluso se aplica a proyectos clásicos.  

Conclusión 

Como probablemente ya haya entendido, no debe temer a la programación funcional.  

 Aquí hay algunos consejos para aquellos que han decidido probarse en un nuevo paradigma y aprender algo radicalmente nuevo: 

Al principio será muy complicado, teniendo en cuenta que tendrás que dejar atrás lo que sabes y aprender nuevos enfoques y principios.  

  • Comience con micro tareas para tenerlo en sus manos. 
  • Si vienes de Java, scala es un gran aleado para empezar, podemos dividir nuestras soluciones y pensar parte en funcional, parte en objeto. Y en .net tenemos F# que es un gran lenguaje.  

La programación funcional tiene un crecimiento constante y es una opción real a problemas que con otros paradigmas no han encontrado una correcta solución.  


lunes, 4 de julio de 2022

Mónadas en Cats parte 15

Como todas las mónadas, los métodos map y flatMap de Eval agregan cálculos a una cadena. En este caso, sin embargo, la cadena se almacena explícitamente como una lista de funciones. Las funciones no se ejecutan hasta que llamamos al método de valor de Eval para solicitar un resultado:

val greeting = Eval.always{ println("Step 1"); "Hello" }.map{ str => println("Step 2"); s"$str world" }

// greeting: Eval[String] = cats.Eval$$anon$4@496b9f25

greeting.value

// Step 1

// Step 2

// res16: String = "Hello world"


Si bien se mantiene la semántica de las instancias de Eval de origen, las funciones de map siempre se llaman de forma perezosa bajo demanda:


val ans = for {

    a <- Eval.now{ println("Calculating A"); 40 }

    b <- Eval.always{ println("Calculating B"); 2 }

} yield { 

    println("Adding A and B")

    a + b

}

// Calculating A

// ans: Eval[Int] = cats.Eval$$anon$4@6e0e633

ans.value // first access

// Calculating B

// Adding A and B

// res17: Int = 42 // first access

ans.value // second access

// Calculating B

// Adding A and B

// res18: Int = 42


Eval tiene un método memoize que nos permite memorizar una cadena de cálculos. El resultado de la cadena hasta la llamada para memorizar se almacena en caché, mientras que los cálculos posteriores a la llamada conservan su semántica original:

val saying = Eval.always{ println("Step 1"); "The cat" }

  .map{ str => println("Step 2"); s"$str sat on" }.memoize

  .map{ str => println("Step 3"); s"$str the mat" }

// saying: Eval[String] = cats.Eval$$anon$4@77e677ee

saying.value // first access

// Step 1

// Step 2

// Step 3

// res19: String = "The cat sat on the mat" // first access

saying.value // second access

// Step 3

// res20: String = "The cat sat on the mat"


¿Que es Kotlin?

 


¿De qué se trata Kotlin? Es un nuevo lenguaje de programación dirigido a la plataforma Java. Kotlin es conciso, seguro, pragmático y se centra en la interoperabilidad con el código Java. Se puede usar en casi todos los lugares donde se usa Java hoy en día: para desarrollo del lado del servidor, aplicaciones de Android y mucho más. Kotlin funciona muy bien con todas las bibliotecas y frameworks Java existentes y se ejecuta con el mismo nivel de rendimiento que Java.

Comencemos con un pequeño ejemplo para demostrar cómo se ve Kotlin. Este ejemplo define una clase Person, crea una colección de personas, encuentra la más vieja e imprime el resultado. Incluso en este pequeño fragmento de código, puedes ver muchas características interesantes de Kotlin. Si quieres ejecutar este ejemplo, la opción más sencilla es utilizar https://play.kotlinlang.org/

data class Person(val name: String, val age: Int? = null)

fun main(args: Array<String>) {

   val persons = listOf(Person("Alice"), Person("Bob", age = 29))

   val oldest = persons.maxBy { it.age ?: 0 }

   println("The oldest is: $oldest")

}

// The oldest is: Person(name=Bob, age=29)

Que estamos haciendo aquí? Se declara una clase con dos propiedades: nombre y edad. La propiedad de edad es nula de forma predeterminada (si no se especifica). Al crear la lista de personas, omite la edad de Alice, por lo que se utiliza el valor predeterminado null. Luego usa la función maxBy para encontrar a la persona más vieja en la lista. La expresión lambda que se pasa a la función usa como el nombre predeterminado de ese parámetro el "it". El operador de Elvis (?:) devuelve cero si la edad es nula. Debido a que no se especifica la edad de Alice, el operador de Elvis la reemplaza con cero, por lo que Bob es el más viejo. 

En fin, un ejemplo vale más que mil palabras, y se puede ver que Kotlin cumple con ser conciso, seguro y pragmático.


sábado, 2 de julio de 2022

Mónadas en Cats parte 14

cats.Eval es una mónada que nos permite abstraernos sobre diferentes modelos de evaluación. Por lo general, hablamos de dos modelos de este tipo: eager y lazy, también llamados llamada por valor y llamada por nombre, respectivamente. Eval también permite memorizar un resultado, lo que nos brinda una evaluación de llamada por necesidad. Eval también es seguro para la pila, lo que significa que podemos usarlo en recursiones muy profundas sin explotar la pila.

¿Qué significan estos términos para modelos de evaluación? Veamos algunos ejemplos. Veamos primero Scala vals. Podemos ver el modelo de evaluación utilizando un cálculo con un efecto secundario visible. En el siguiente ejemplo, el código para calcular el valor de x ocurre en el lugar donde se define en lugar de acceder. Acceder a x recupera el valor almacenado sin volver a ejecutar el código.

val x = {

    println("Computing X")

    math.random

}

// Computing X

// x: Double = 0.0396922778013471

x // first access

// res0: Double = 0.0396922778013471

x // second access

// res1: Double = 0.0396922778013471


Este es un ejemplo de evaluación de llamada por valor:

• el cómputo se evalúa en el punto donde se define (eager); y

• el cálculo se evalúa una vez (se memoriza).

Veamos un ejemplo usando un def. El código para calcular y a continuación no se ejecuta hasta que lo usamos y se vuelve a ejecutar en cada acceso:


def y = {

    println("Computing Y")

    math.random

}

y // first access

// Computing Y

// res2: Double = 0.5270290953284378 

y // second access

// Computing Y

// res3: Double = 0.348549829974959


Estas son las propiedades de la evaluación llamada por nombre:

• el cálculo se evalúa en el punto de uso (lazy); y

• el cálculo se evalúa cada vez que se utiliza (no se memoriza).

Por último, pero no menos importante, los valores perezosos son un ejemplo de evaluación de llamada por necesidad. El código para calcular z a continuación no se ejecuta hasta que lo usamos por primera vez (perezoso). Luego, el resultado se almacena en caché y se reutiliza en accesos posteriores (memorizados):


lazy val z = {

    println("Computing Z")

    math.random

}

z // first access

// Computing Z

// res4: Double = 0.6672110951657263 // first access

z // second access

// res5: Double = 0.6672110951657263


Resumamos. Hay dos propiedades de interés:

• evaluación en el punto de definición (ansioso) versus en el punto de uso (perezoso); y

• los valores se guardan una vez evaluados (memorizados) o no (no memorizados).

Hay tres posibles combinaciones de estas propiedades:

• llamada por valor que está ansiosa y memorizada;

• llamada por nombre que es perezosa y no memorizada; y

• llamar por necesidad, que es perezoso y memorizado.

La combinación final, ansiosa y no memorizada, no es posible.


Eval tiene tres subtipos: Now, Always, y Later. Corresponden a llamada por valor, llamada por nombre y llamada por necesidad, respectivamente. Los construimos con tres métodos constructores, que crean instancias de las tres clases y las devuelven escritas como Eval:

import cats.Eval

val now = Eval.now(math.random + 1000)

// now: Eval[Double] = Now(1000.7009661848473)

val always = Eval.always(math.random + 3000)

// always: Eval[Double] = cats.Always@2a4e7955

val later = Eval.later(math.random + 2000)

// later: Eval[Double] = cats.Later@7684da18


Podemos extraer el resultado de un Eval usando su método de valor:


now.value

// res6: Double = 1000.7009661848473

always.value

// res7: Double = 3000.5158510235524

later.value

// res8: Double = 2000.6995448328964


Cada tipo de Eval calcula su resultado utilizando uno de los modelos de evaluación definidos anteriormente. Eval.now captura un valor ahora mismo. Su semántica es similar a un val: ansioso y memorizado:

val x = Eval.now{

println("Computing X")

math.random

}

// Computing X

// x: Eval[Double] = Now(0.6969571260771719)

x.value // first access

// res10: Double = 0.6969571260771719 // first access

x.value // second access

// res11: Double = 0.6969571260771719


Eval.always captura un cálculo perezoso, similar a una definición:


val y = Eval.always{

println("Computing Y")

math.random

}

// y: Eval[Double] = cats.Always@6d355284

y.value // first access

// Computing Y

// res12: Double = 0.8575236846076497 // first access

y.value // second access

// Computing Y

// res13: Double = 0.15716382484701563


Finalmente, Eval.later captura un cálculo memorizado perezoso, similar a un valor perezoso:


val z = Eval.later{

println("Computing Z")

math.random

}

// z: Eval[Double] = cats.Later@3429dabc

z.value // first access

// Computing Z

// res14: Double = 0.5149108999064906 // first access

z.value // second access

// res15: Double = 0.5149108999064906



viernes, 1 de julio de 2022

[eBook] The Secret to Modern Application Security

 

EBOOK

[eBook] The Secret to Modern Application Security

Hi Emanuel,

Every business strives for sustained agility. Many see the benefits of a distributed microservices architecture, using modern applications and APIs to keep pace with the competition and customer demand. In fact, 85% of new workloads are deployed in containers and 83% of internet traffic is made up of API calls. In this eBook you will learn how NGINX App Protect WAF can be deployed with NGINX Ingress Controller to protect your apps and APIs in Kubernetes clusters, enforce security policies, inspect traffic, and eliminate threats before they cross the perimeter, allowing you to innovate at speed without compromising on security.

In this eBook you will learn:

  • Why integrating security automation into your CI/CD pipeline is key to the agile software development of your applications and APIs
  • Ways the performance of your application directly impacts customer experience and sales
  • About implementing strong security into an application’s operating system using NGINX App Protect WAF, which supports multiple modern application deployment topologies
  • How NGINX App Protect WAF works with NGINX Ingress Controller as the gatekeeper for an entire Kubernetes cluster and supports enforcing policies at a granular level within the cluster