Skip to content

Instantly share code, notes, and snippets.

@Amgelo563
Last active September 10, 2024 02:05
Show Gist options
  • Select an option

  • Save Amgelo563/b1005cdf035d5a44efaf377ece51c702 to your computer and use it in GitHub Desktop.

Select an option

Save Amgelo563/b1005cdf035d5a44efaf377ece51c702 to your computer and use it in GitHub Desktop.
Especificación del formato HOCON.

hocon spec

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.

Definiciones

Algunas definiciones son necesarias para entender esta documentación:

  • Una key es una string en un objeto que está a la izquierda de un :. Su value es lo que esté a la derecha de este. Juntos hacen un object field o solo field.
  • Un simple value o value simple es cualquier tipo de value excepto un objeto o una array.
  • Las references a un file implican cualquier stream de bytes siendo parseado, no necesariamente solo archivos en un filesystem.
  • El whitespace es cualquiera de estos caracteres: (espacio), \n (enter o linefeed), \r (carriage return), (⭾ TAB).

JSON

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).

Comentarios

Los comentarios se definen con // o #. No existen comentarios multi-línea.

Llaves root opcionales

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"

Separador Key-Value

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"
  }
}

Comas

  • 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.

Keys duplicados

  • 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 }
}

Strings sin comillas

Una string (incluyendo keys) puede omitir las comillas si:

Strings multi-línea

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.

Substituciones

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!

Substituciones: Substitución opcional

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.

Substituciones: Substitución auto referenciada

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}bc
  • path: ${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.

Concatenación

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.

Concatenación: Concatenación de arrays

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" ]

Concatenación: Concatenación de objetos

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" }

Rutas como keys

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 }

Includes

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.

Includes: Sintaxis

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(...) o classpath(...) 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" ]

Includes: Comportamiento

  1. El archivo incluido es parseado internamente, produciendo un objeto nuevo.
  2. El include statement es reemplazado por las keys de este objeto, siguiendo la siguiente regla.
  3. 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.

Includes: Substituciones

Las substituciones de archivos incluidos son buscadas en el siguiente orden:

  1. En el propio archivo, como funciona normalmente.
  2. 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, pues b se substituye con "Valor A", el valor de a.
  • Si parseamos bar.conf, la substitución de b estaría errónea, pues la ruta de a es baz.a, no solo a (ruta la cual no existe). Esto se debe tomar en cuenta si se decide incluir archivos que usan substituciones en isolación.

Includes: Archivos requeridos y faltantes

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"))

Includes: Tipos de archivos soportados

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.

Includes: Resolución

Cuando se usa una string como argumento (sin url(), file() o classpath()), esta se intenta interpretar como (en el siguiente orden):

  1. Una URL si es una URL válida, con un protocolo conocido por el engine (la mayor parte del tiempo http/s).
  2. Una ruta a un archivo.
  3. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment