En esta guía se documenta el formato HOCON.
Aunque se documentan la gran mayoría de features de HOCON, esto no es un reemplazo de la spec oficial. Esta versión está simplificada para ser más corta y entendible por la mayoría de usuarios, aunque sí se espera tener conocimiento básico de dev.
Adaptado de la spec oficial de HOCON.
Algunas definiciones son necesarias para entender esta documentación:
- Una
keyes una string en un objeto que está a la izquierda de un:. Suvaluees lo que esté a la derecha de este. Juntos hacen unobject fieldo solofield. - Un
simple valueovalue simplees cualquier tipo de value excepto un objeto o una array. - Las
referencesa unfileimplican cualquier stream de bytes siendo parseado, no necesariamente solo archivos en un filesystem. - El
whitespacees cualquiera de estos caracteres:(espacio),\n(enter o linefeed),\r(carriage return),(⭾ TAB).
HOCON es superset de JSON: todo JSON válido es HOCON válido. En específico, las cosas que no han cambiado son:
- Las strings con
""siguen el mismo formato que JSON. - Los tipos de dato posibles siguen siendo string, number, object, array, boolean o null.
- Los números siguen el mismo formato de JSON, incluyendo soporte de decimales y exponentes.
En esta guía solo se documentarán los cambios en relación a JSON, no el formato entero (pues mucho de ello sería documentar JSON).
Los comentarios se definen con // o #. No existen comentarios multi-línea.
En JSON, todo archivo debe empezar con llaves {} (para objetos) o corchetes [] (para arrays).
En HOCON, son opcionales (como si fuera YAML), y si un archivo no inicia con estas, se va a asumir que es un objeto.
Es decir, estos dos archivos son iguales:
JSON:
{
"foo": "bar"
}HOCON:
"foo": "bar"Se pueden usar tanto : como = para el separador. Si el valor es un objeto, se pueden omitir.
Es decir, estos tres son iguales:
JSON:
{
"foo": {
"bar": "baz"
}
}HOCON:
{
"foo": {
"bar" = "baz"
}
}
{
"foo" {
"bar": "baz"
}
}- Los valores en arrays y los fields en objects no necesitan coma si hay un enter (
\n) entre ellos. - Puede haber una coma extra al final de un array o en el último field. Esta coma va a ser ignorada.
- En caso que hayan dos keys iguales, el valor del último va a prevalecer sobre el otro.
- Solo hay una excepción en caso los values de las dos keys son objects. En este caso, ambos objetos son fusionados en uno:
- Un field presente en solo uno de ellos es agregado al resultado.
- Para un field presente en ambos (y ambos values no sean objetos), el value del segundo objeto es usado.
- En caso sí sean objetos, son fusionados con las mismas reglas anteriores.
Es decir, estos dos son iguales:
{
"foo" : { "a" : 42 },
"foo" : { "b" : 43 }
}
{
"foo" : { "a" : 42, "b" : 43 }
}Y estos dos son iguales:
{
"foo" : { "a" : 42 },
"foo" : null,
"foo" : { "b" : 43 }
}
{
"foo" : { "b" : 43 }
}Una string (incluyendo keys) puede omitir las comillas si:
- No contiene caracteres prohibidos: '$', '"', '{', '}', '[', ']', ':', '=', ',', '+', '#', '`', '^', '?', '!', '@', '*', '&', \ o espacios.
- No contiene // que inicia un comentario.
- No inicia con
true,false,nullo un número (incluyendo-, que sería un número negativo). Esta documentación, aunque sirve, está muy simplificada, recomendaría revisar el spec oficial.
Se puede crear con """, por ejemplo:
foo: """
Esta es una string con
varias líneas.
"""Nótese que el whitespace no es ignorado, y se considera como parte de la string. Por ejemplo:
foo: """
Esta es una string con
varias líneas.
"""Se parsea a:
Esta es una string con
varias líneas.
Esto en Markdown podría causar listas o aperturas de código no intencionadas. Para evitar inconvenientes, recomiendo regresar siempre al inicio de línea, como en el primer ejemplo.
Una substitución es una forma de usar un valor de otra parte de la configuración. Las substituciones solo están permitidas el values y elementos de arrays.
Por ejemplo:
usuario: {
nombre: "Pepe"
}
# La ubicación es absoluta, no relativa.
saludo: Hola ${usuario.nombre}! # Hola Pepe!Nótese la falta de comillas en el saludo. En específico, una substitución no se va a realizar si está dentro de una string con comillas.
Por ejemplo:
saludo: "Hola ${usuario.nombre}!" # Hola ${usuario.nombre}!
saludo: "Hola " ${usuario.nombre} "!" # Hola Pepe!
Un tipo de substitución especial es la "substitución opcional", definida con ? antes de la ubicación. Por ejemplo, $?{usuario.nombre}.
Este tipo de substitución funciona igual si el valor referido existe, pero si no existe:
- Si es un valor en un field, este field no es creado si no existía antes. Si existía, su valor no es reemplazado:
# Compila a { baz: "valor existente" }
foo: {
bar: ${?valorNoExistente}
baz: "valor existente"
}
# Compila a { bar: "no reemplazado" }
foo: {
bar: "no reemplazado"
bar: ${?valorNoExistente}
}- Si está en un array, el elemento no es añadido.
# Compila a []
[ ${?valorNoExistente} ]- Si es parte de una concatenación:
- Con un objeto o string, se volverá un objeto vacío o una string vacía, respectivamente.
- Con una substitución indefinida y es usado como value, no se creará el field.
Una substitución auto referenciada es un valor que:
- Tiene una substitución o concatenación que contiene una substitución.
- Dicha substitución eventualmente llega al mismo valor siendo definido, ya sea directa o a través de un "ciclo" de substituciones.
Algunos ejemplos:
a: ${a}a: ${a}bcpath: ${path} [ /usr/bin ]
Objetos o arrays con una substitución dentro no son contados como auto referenciadas, por ejemplo esto va a dar error, ya que no se puede encontrar un valor apropiado:
a: { b: ${a} }a : [${a}]
En caso una substitución sea "reemplazada" luego, el valor inicial no va a ser evaluado. Por ejemplo:
a: ${a}
a: "b"No da error, pues el "b" "sobreescribe" el valor de a, y la substitución con error ${a} nunca es procesada.
Un value en un objeto o en un array puede consistir de varios values a concatenar. Hay tres tipos de concatenación:
- Concatenación de strings (no se cubre en esta guía, en esencia es lo que permite el uso de strings sin comillas).
- Concatenación de arrays.
- Concatenación de objects.
Para concatenar dos arrays, se incluyen los valores de ambos. Es decir, estos tres son iguales:
// 1:
foo: [ 1, 2, 3, 4 ]
// 2:
foo: [ 1, 2 ] [ 3, 4 ]
// 3:
foo: [ 1, 2 ]
foo: ${foo} [ 3, 4 ]Un uso común es concatenar paths:
path: [ "/bin" ]
path: ${path} [ "/usr/bin" ]Sigue las mismas reglas descritas anteriormente en la sección de keys duplicados.
Es decir, estos tres son iguales:
// 1:
foo: { b: 1, c: 2 }
// 2:
foo: { b: 1 } { c: 2 }
// 3:
foo: { b: 1 }
foo: { c: 2 }Un uso común es para "simular" inheritance usando substituciones:
data-center-generic: { cluster-size: 6 }
data-center-east: ${data-center-generic} { name: "east" }Si una key es una ruta, se usará esa ruta para crear objetos anidados hasta la ubicación final. En caso el objeto ya existía, simplemente se añade el value.
En otras palabras, estos dos son iguales:
// 1
foo: { bar: "baz" }
// 2
foo.bar: "baz"Y estos dos también:
// 1
foo: { bar: { baz: "qux" } }
// 2
foo.bar.baz: "qux"Ya que ambos son equivalentes, también siguen las reglas normales de objetos, por ejemplo la concatenación:
// Esto:
a.x: 42
a.y: 43
// Equivale a:
a: { x: 42, y: 43 }La API de include te permite "incluir" recursos externos dentro de la configuración, normalmente otra configuración HOCON. Esta API se usa con un include statement.
Un include statement consiste en la palabra include seguido de:
- Una string con comillas que es una URL, ubicación de archivo o recurso en el classpath (Java).
url(...),file(...)oclasspath(...)que contengan una string con comillas que sea una URL, nombre de archivo o un recurso en el classpath (Java); respectivamente.required(...)que contenga cualquiera de los de arriba.
Warning
En HOCON vanilla, las concatenaciones no están permitidas dentro del argumento del include. Este argumento debe ser una string como tal sin más, sin concatenaciones, substituciones, etc.
Sin embargo, en HOCON por hocon-parser sí se permiten las substituciones, pero esto es algo de solo ese engine. En cualquier otro deberías buscar la documentación, o asumir que no se permiten.
Tip
La ubicación de un archivo (file(...)) o de un recurso en el classpath (classpath(...)) puede ser absoluta si inicia con / o relativa en caso contrario.
Un include se escribe en el lugar donde normalmente iría un field. Por ejemplo:
foo: {
include "bar.conf"
}O simplemente en root, ya que las llaves root son opcionales:
include "bar.conf"Mientras que estos no son considerados include statements:
{ foo include: "bar" } // Equivalente a { "foo include": "bar" }
{ foo: include } // Equivalente a { foo: "include" }
[ include ] // Equivalente a [ "include" ]- El archivo incluido es parseado internamente, produciendo un objeto nuevo.
- El include statement es reemplazado por las keys de este objeto, siguiendo la siguiente regla.
- Si la key está presente (ya sea dentro del objeto o por un include statement anterior) entonces esta key es reemplazada o fusionada, siguiendo la misma lógica de keys duplicados.
Warning
En HOCON vanilla, no está permitido que un archivo incluido sea un array. Sí o sí tienen que ser objetos, lo cual está explícitamente resaltado en la spec oficial.
Sin embargo, en HOCON por hocon-parser sí se permiten incluir arrays, pero esto es algo de solo ese engine. En cualquier otro deberías buscar la documentación, o asumir que no se permiten.
Las substituciones de archivos incluidos son buscadas en el siguiente orden:
- En el propio archivo, como funciona normalmente.
- Si no se encontró, en el archivo que está incluyendo al incluido.
Téngase en cuenta que las substituciones pasan luego del parseo, lo cual incluye el include como tal. Esto se debe tomar en cuenta para ciertos casos, por ejemplo:
foo.conf
a: "Valor A"
b: ${a}bar.conf
baz {
include "foo.conf"
}En este caso, el comportamiento es diferente dependiendo de si parseamos solo foo.conf o bar.conf:
- Si parseamos
foo.conf, no hay ningún problema, puesbse substituye con"Valor A", el valor dea. - Si parseamos
bar.conf, la substitución debestaría errónea, pues la ruta deaesbaz.a, no soloa(ruta la cual no existe). Esto se debe tomar en cuenta si se decide incluir archivos que usan substituciones en isolación.
Por defecto, si un archivo incluido no existe entonces el include statement es ignorado (ya que se interpreta como si el archivo estuvo vacío).
Sin embargo, si deseas hacerlo requerido, puedes envolverlo en un required(...), en cuyo caso el parseo fallará si no existe. La sintaxis es:
include required("foo.conf")
include required(file("foo.conf"))
include required(classpath("foo.conf"))
include required(url("http://localhost/foo.conf"))Warning
En esta sección se usan bastante las palabras "debería", "podría", etc. Esto es porque esta sección depende enteramente del engine, y cada implementación decide cómo manejar esta parte. Lo descrito aquí es lo descrito por la spec oficial de HOCON.
Los tipos de archivos soportados por include dependen del engine. Si una implementación soporta múltiples tipos, entonces se puede debería poder omitir la extensión al incluir el archivo:
include "foo"
En estos casos el engine debería intentar cargar el archivo con todas las extensiones posibles, primero en las extensiones adicionalmente soportadas, penúltimo JSON (.json) y último HOCON (.conf). Si hay múltiples archivos encontrados, todos deberían ser fusionados en uno solo.
Para URLs, el tipo elegido depende del header Content-Type o de la extensión en el link, dependiendo del que esté presente.
Cuando se usa una string como argumento (sin url(), file() o classpath()), esta se intenta interpretar como (en el siguiente orden):
- Una URL si es una URL válida, con un protocolo conocido por el engine (la mayor parte del tiempo http/s).
- Una ruta a un archivo.
- Dependiendo del engine, se puede optar por intentar buscarlo como un recurso en el classpath.
Para ver más detalles de la resolución, revisa la spec oficial.