En toda aplicación, es común realizar la carga de configuración desde variables de entorno.En el ecosistema de Scala, la tarea de carga de estos valores los podemos realizar con la solución Ciris. Ciris es una librería funcional que realiza esta funcionalidad. En la presente entrada, Ciris, realizaré una breve descripción de esta librería sencilla.
Definición de dependencias
Ciris está compuesta por un conjunta de paquetes que utilizan otros módulos, como pueden ser: Circe, Refined o Squants; pero, para poder trabajar correctamente, es necesario añadir el paquete refined-cats el cuál no se referencia en la documentación.
La definición de las dependencias necesarias para trabajar con Ciris son las siguientes:
lazy val ciris_ciris = "is.cir" %% "ciris" % "1.2.1" lazy val ciris_circe = "is.cir" %% "ciris-circe" % "1.2.1" lazy val ciris_enumeratum = "is.cir" %% "ciris-enumeratum" % "1.2.1" lazy val ciris_refined = "is.cir" %% "ciris-refined" % "1.2.1" lazy val ciris_squants = "is.cir" %% "ciris-squants" % "1.2.1" lazy val ciris_refined_cats = "eu.timepit" %% "refined-cats" % "0.9.18"
Para cargar las dependencias definidas anteriormente en el proyecto, modificamos el fichero build.sbt para realizar la carga del módulo y realizar los ejemplos básicos. El código resultante del fichero de configuración sbt es el siguiente:
import Dependencies._ [...] lazy val ciris = (project in file("ciris")) .settings( name := "example-ciris", assemblySettings, scalacOptions ++= basicScalacOptions, libraryDependencies ++= cirisDependencies ++ Seq( scalaTest ) ) lazy val cirisDependencies = Seq( ciris_ciris ,ciris_circe ,ciris_enumeratum ,ciris_refined ,ciris_squants ,ciris_refined_cats )
Casos de uso de ejemplo
En el presente apartado, realizaré la presentación de unos ejemplos de prueba de la librería Ciris.
- Carga de una variable de entorno de tipo entera
Sea la variable de entorno API_PORT=8080 y el siguiente snippet de código el cual realiza lo siguiente: lectura de la varibla API_PORT como entero creando un objeto port de tipo ConfigValue[Int]; el objeto port, es cargado como un tipo IO de cats-effect y la función unsafeRunSync resuelve el valor del tipo IO obteniendo el valor resultado.
def exampleLoadIntENV(): Unit = { val port: ConfigValue[Int] = env("API_PORT").or( prop("api.port") ).as[Int] val portResult: Int = port.load[IO].unsafeRunSync() println(s"Port=${portResult}") println() }
El resultado por pantalla es el siguiente:
Port=8080
- Carga de variables de entorno en una clase.
Sean las variables de entorno API_PORT=8080 y API_TIMEOUT=100 millis y el siguiente snippet de código el cual realiza lo siguiente: lectura de la varibla API_PORT como entero creando un objeto de tipo ConfigValue[Int]; lectura de la variable API_TIMEOUT de tipo Duration de tipo ConfigValue[Duration]; definición de la clase Config con un campo de tipo entero y otro de tipo Duration; parseo de los dos objetos anteiores para carga los valores de tipo ConfigValue[A] a la clase Config; y, para finalizar, carga del objeto de tipo ConfigValue[Config] como un tipo IO de cats-effect y la función unsafeRunSync resuelve el valor del tipo IO obteniendo el valor resultado.
def exampleLoadPairEnvVar(): Unit = { val port: ConfigValue[Int] = env("API_PORT").or( prop("api.port") ).as[Int] val timeout: ConfigValue[Option[Duration]] = env("API_TIMEOUT").as[Duration].option final case class Config(port: Int, tiemout: Option[Duration]) val config: ConfigValue[Config] = (port, timeout).parMapN(Config) val resultConfig: Config = config.load[IO].unsafeRunSync() println(s"Result Config->${resultConfig}") println() }
El siguiente snippet de código es idéntico al anterior pero se utiliza for comprehension.
def exampleLoadWithForCom(): Unit = { final case class Config(port: Int, tiemout: Option[Duration]) val config = for{ eport <- env("API_PORT").or( prop("api.port") ).as[Int] etimeout <- env("API_TIMEOUT").as[Duration].option } yield{ Config(eport, etimeout) } val result: Config = config.load[IO].unsafeRunSync() println(s"Result Config->${result}") println() }
La salida por consola es la siguiente:
Result Config->Config(8080,Some(100 milliseconds))
- Carga de variable de entorno y valor por defecto
Sean las variables de entorno API_PORT=8080 y API_TIMEOUT=100 millis. Los siguientes snippet de código son idénticos a los anteriores salvo que se utiliza la función default para determinar el valor por defecto: en el primer caso, con un valor de tipo Duration; y, en el segundo, con una clase Config.
def exampleDefaultValue1(): Unit = { val timeDefault: ConfigValue[Duration] = env("API_TIME_DEEFAULT").as[Duration].default(10.seconds) val result: Duration = timeDefault.load[IO].unsafeRunSync() println(s"Result default 1=${result}") println() } def exampleDefaultValue2(): Unit = { final case class Config(port: Int, tiemout: Option[Duration]) val config = ( env("API_PORT").or( prop("api.port") ).as[Int], env("API_TIMEOUT").as[Duration].option ).parMapN(Config).default{ Config(8082, 20.seconds.some) } val result: Config = config.load[IO].unsafeRunSync() println(s"Result default 2=${result}") println() }
La salida por consola es la siguiente:
Result default 1=10 seconds Result default 2=Config(8080,Some(100 milliseconds))
- Carga de variables de entorno con un secreto
Sea la variable de entorno API_KEY=keyRR01234567890123456789. El siguiente snippet realiza la carga de una variable de entorno que representa un secreto; en este caso, se emplea la función secret la cual retorno un objeto de tipo Secret[String]
def exampleSecrets(): Unit = { val apiKey: ConfigValue[Secret[String]] = env("API_KEY").secret val resultSecret: Secret[String] = apiKey.load[IO].unsafeRunSync() println(s"secret=${resultSecret.value}") println() }
La salida por consola es la siguiente:
secret=keyRR01234567890123456789
Para el lector interesado puede acceder al código a través del siguiente enlace.