Conferer, una biblioteca de configuración para Haskell
Conferer es una biblioteca que ayuda a configurar tus aplicaciones en Haskell con una interfaz ergónomica.
If want to see this post in English, click here.
Los desafíos de configurar aplicaciones en Haskell
En toda aplicación suficientemente grande llega el momento en el que hay que manejar configuración. Si querés saber más sobre por que querrías configuración en tus proyectos te invitamos a leer este post.
En particular en Haskell, algunas bibliotecas (hspec, snap, etc) proveen funciones para obtener configuración ya sea a través de env vars, de un archivo o de otra manera. Sin embargo, cada una lo hace de una forma diferente (y otras ni siquiera proveen esto) así que si querés obtener toda la configuración para tu programa de un solo lugar vas a tener que programarlo a mano.
Esto implica que cada vez que implementés la configuracion en una aplicación nueva vas a tener que resolver los mismos problemas una y otra vez: manejo de errores, de valores por defecto, de diferentes ambientes, el parseo de la configuración en valores de tu programa, etc.
Una solución: Conferer
Conferer es una biblioteca que ayuda a configurar tus aplicaciones en Haskell con una interfaz ergónomica.
Esto lo logra:
- Dejándote definir diferentes fuentes de donde obtener la configuración.
- Resolviendo el parseo de configuración en valores válidos de tu programa.
- Permitiéndote configurar valores por defecto.
Ejemplo
En este ejemplo vamos a ver a alto nivel varias de las funcionalidades de Conferer.
Digamos que queremos crear un servidor HTTP al cual le queremos especificar tanto el puerto como un mensaje inicial que se va a mostrar cada vez que lo arranquemos.
Definiendo de donde leemos la configuración
Lo primero que necesitamos es definir de donde va a venir la configuración. Conferer nos permite hacer esto de manera declarativa, lo único que hay que hacer es listar las fuentes.
En el siguiente código, definimos una configuración que lee de la línea de comandos, de variables de ambiente y de un archivo Dhall llamado "config.dhall".
mkMyConfig :: IO Config
mkMyConfig = mkConfig' []
[ Cli.fromConfig
, Env.fromConfig "myapp"
, Dhall.fromFilePath "config.dhall"
]
Cuando intentemos obtener un valor de la configuración se va a intentar buscar en las fuentes en el orden que están listadas, por lo que también estamos declarando su prioridad en la lista. En este ejemplo, si quisiesemos sobre-escribir algún valor del archivo de Dhall podríamos hacerlo pasando ese valor como un parámetro de línea de comandos.
Interpretando la configuración
La typeclass FromConfig
es usada cuando pedimos valores de nuestra configuración, por lo que vamos a necesitar una instancia de la misma para los tipos de todo lo que queramos que sea configurable. Conferer ya viene con algunas instancias de FromConfig
para tipos comunes como String
, Int
, Bool
, etc. Y si nuestro tipo tiene una instancia de Generic
podemos derivar la instancia de FromConfig
así:
data AppConfig = AppConfig
{ appConfigServer :: Warp.Settings
, appConfigBanner :: String
} deriving (Generic)
instance Conferer.FromConfig AppConfig
Además, se pueden agregar nuevas instancias de FromConfig
para configuraciones de bibliotecas comunes. De hecho, algunas ya están provistas en diferentes paquetes (conferer-warp
, conferer-hedis
, etc).
Configurando valores por defecto
No queremos que el usuario tenga que configurar absolutamente todo.
Podemos proveer valores por defecto para cada valor de la configuración definiendo una instancia de DefaultConfig
para el tipo de nuestra configuración.
instance Conferer.DefaultConfig AppConfig where
configDef = AppConfig
{ appConfigServer = Conferer.configDef
, appConfigBanner = "Hi conferer"
}
Esto le permite al usuario pasar solo algunos de los parámetros y dejar que los valores por defecto se encarguen del resto. Por ejemplo, si corriésemos nuestro programa así:
$ ./conferer-example --server.port=2222
se van a usar todos los ajustes por defecto del server de warp excepto el puerto que es lo que pasamos explícitamente.
Detectando cuando las cosas salen mal con mensajes de error
Si la configuración provista es invalida, Conferer va a fallar con un error descriptivo tan pronto como trate de procesarla.
$ ./conferer-example --server.port=fewa
conferer-example: Couldn't parse value 'fewa' from key '"server.port"' as Int
Usando los valores configurados
Una vez definidas las fuentes, implementados los FromConfig
s necesarios y elegidos los defaults, es momento de usar los valores que Conferer obtiene:
main :: IO ()
main = do
config <- mkMyConfig
appConfig <- Conferer.fetch config
let warpSettings = appConfigServer appConfig
putStrLn $ appConfigBanner appConfig
putStrLn $ "Running on port: " ++ show (Warp.getPort $ warpSettings)
Warp.runSettings warpSettings application
Este ejemplo muestra como una vez obtenidos los valores de la configuración usando Conferer se puede escribir lógica del negocio que sea agnóstica a de donde salieron esos valores.
Conclusión
En este ejemplo construimos un programa donde el manejo de la configuración no se mete en el medio del código de dominio que queremos escribir.
Adicionalmente, tanto las fuentes como las instancias de FromConfig
pueden ser implementadas como paquetes independientes, permitiendo que cualquiera extienda Conferer.
Un ejemplo completo del código mostrado se puede encontrar acá. También, si querés un tutorial guíado, podés revisar esto.
Para cerrar
Ya estamos usando Conferer en algunas aplicaciones internas en 10Pines y hasta ahora demostró ser útil para facilitar la configuración de nuestras aplicaciones escritas en Haskell.
Hoy estamos anunciando la primer versión estable y planeamos seguir trabjando en mejoras, por lo que cualquier comentario o sugerencia es bienvenido.
Saludos!