npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@pushcorn/hocon-parser

v1.3.1

Published

A HOCON parser

Downloads

7,607

Readme

A HOCON Parser for Node.js

Node version Build Status Coverage Status install size Known Vulnerabilities

hocon-parser is a lightweight and extensible JavaScript parser for HOCON (Human-Optimized Config Object Notation). It can be used as a library or invoked via the CLI. It also supports data transformation (with a pipe "|" symbol) that allows you to transform the data into different types. Developer are free to extend core classes to achieve different parsing behavior.

Table of Contents

Installation

To use hocon-parser as library, go to your project directory and run the following command:

npm install @pushcorn/hocon-parser

The parser also comes with a CLI command parse-hocon. Run the following command to install the parser globally:

npm install -g @pushcorn/hocon-parser

To parse a config file named test.conf, just use the following command:

parse-hocon test.conf

You can also pass the config text to the program with the -t switch:

parse-hocon -t "a: 1"

The output of the command is a formatted JSON object:

{
  "a": 1
}

Examples

A lot of examples can be found in the tests/resources/configs directory. Each subdirectory should contain a file test.conf and optionally a result file result.json. If a directory name contains the .error suffix, that means parsing the test file test.conf will result in an error. By default, the files are parsed in strict mode when testing. If the directory name has a .default suffix, the config file will be parsed in the default mode. Below is a list of some advanced features that the parser supports.

Array Element Accessing

Input:

b = [${f}]
f = { nested: { g: 9 } }
this_is_9_from_b_0 = ${b.0.nested.g}

Output:

{
  "b": [
    {
      "nested": {
        "g": 9
      }
    }
  ],
  "f": {
    "nested": {
      "g": 9
    }
  },
  "this_is_9_from_b_0": 9
}

Dynamic Include

Input:

env = development

config {
    include ${env}
}

development.conf:

name: orange
os: linux

Output:

{
  "config": {
    "name": "orange",
    "os": "linux"
  },
  "env": "development"
}

Filter Transform

Input:

a: [1, 2, 3, 4, 5, 6] | filter { expr = "$ > 3" }

Output:

{
  "a": [
    4,
    5,
    6
  ]
}

Array Inclusion

Input:

a: 3
b: include array

array.json

[1, 2]

Output:

{
  "a": 3,
  "b": [
    1,
    2
  ]
}

Key Substitution

Input:

a = x
b = y
${a}.${b} = 33

Output:

{
  "a": "x",
  "b": "y",
  "x": {
    "y": 33
  }
}

Resolution with Local Scope

Input:

hello_david {
  name = "david"
  include "hello.conf"
}

hello_lisa {
  name = "lisa"
  include "hello.conf"
}

hello.conf:

hello {
  msg = hello ${name}
}

Output:

{
  "hello_david": {
    "hello": {
      "msg": "hello david"
    },
    "name": "david"
  },
  "hello_lisa": {
    "hello": {
      "msg": "hello lisa"
    },
    "name": "lisa"
  }
}

Nested Substitution

Input:

region : 2

regions {
    1 : us-east-1
    2 : eu-west-1
    3 : sa-east-1
}

name : ${regions.${region}}

Output:

{
  "name": "eu-west-1",
  "region": 2,
  "regions": {
    "1": "us-east-1",
    "2": "eu-west-1",
    "3": "sa-east-1"
  }
}

Divergence from the HOCON Spec

HOCON is a simple yet powerful configuration language. To make it more powerful, this parser supports an extended inclusion syntax that allows the user to specify how a resource will be included. For example, the following two snippets means the same thing: include /test.conf from localhost and parse the config in the strict mode.

Syntax 1:

include { url = "http://localhost/test.conf", strict: true }

Syntax 2:

include strict(url("http://localhost/test.conf"))

The parser also supports file inclusion at the property level. For example, following snippet can be used to load your server certificates. The parser will check the file extension and choose a proper node builder to parse the content. In this case, both .key and .crt are not known binary extensions, so the parser will load the files in plain text and keys.private and keys.public will both contain a string.

keys {
  private = include host.key
  public = include host.crt
}

To prevent the parser from treating include as a keyword, simple surround the keyword with double quotes.

private = "include" host.key

In addition to the include keyword, you can also use require to require a file. Basically,

include required ("test.conf")

is the same as

require "test.conf"

Another divergence from the spec is that defining an array at root level is allowed. For example, given the following two files

array.conf:

[1, 2, 3]

main.conf:

ints: include array.conf

The output of parse-hocon main.conf will be

{
  "ints": [
    1,
    2,
    3
  ]
}

Parsing Modes

The parser can operate in two modes: default or strict. In the default mode, the parser supports the file inclusion symbol, data transformation, and will try to convert a string value into a JS primitive value when possible. In the strict mode, the parser is more spec-compliant, and some extended syntaxes are not available.

Default Mode

File Inclusion Shorthand

Instead of using the include keyword, you can use @? and @ to include or require a file, respectively.

@"file.conf" # the same as "include required (file.conf)" or "require file.conf"
@?"file.conf" # the same as "include file.conf"

Data Transformation

You can use transforms to quickly map filter an array or turn a value into a different type. To apply a transform, simply put a pipe (|) character followed by the name of the transform you want to use. You can pass an option object to the transform if it is configurable. Applying multiple transforms to a value is also supported. For example, the following snippet will sort the array then extract the last 2 elements from it.

a: [3, 4, 5, 1, 2] | sort | slice { end = -2 }

Output:

{
  "a": [
    4,
    5
  ]
}

Besides the sort and slice transforms, the parser comes with several useful transforms like map, reduce, query, and eval. You can also define your transforms and register them with the parser.registerComponent () method.

Properties File Parsing

According to the spec, the values from a .properties file are always interpreted as strings. In the default mode, the parser will try to convert them into corresponding JS values.

Number Parsing

The spec did not clearly define how an unquoted string .3 is parsed. Some implementations treat it as a string because floats with a leading dot is not allowed by JSON. This parser will interpret it as 0.3 by default.

Strict Mode

The strict mode can be enabled in several ways. When using the CLI, just pass the -s switch to the command.

$ parse-hocon -t "a: .3" -s
{
  "a": ".3"
}

Or just set the strict option to true when using the parser programmatically.

const parse = require ("@pushcorn/hocon-parser");

parse ({ text: "a: .3", strict: true }).then (console.log);

Or wrap the file path with strict() when you include a file.

include strict(file("test.properties"))

Core Components

The parser consists of five major components:

  • Context: The parsing context.
  • Node: The data structure used to represent the parsed config.
  • Source: The data source.
  • Builder: The node builder.
  • Transform: The data transform.

Context

The context is responsible for loading the data from the source and building the root node of the config file. A new context will be created when a file is included. A context also stores some relevant information about the loaded file/resource, like encoding of the file, whether the file is required, or if the file should be parsed in strict mode. The Context class does the heavy lifting of the parsing task. If you check the source code, you will notice that the parser simply delegates the parsing task to the context.

var context = new Context (opts);
var result  = await context.resolve ();

Node

The parser comes with several node classes that are used to represent the data defined in the config file. For example, the ArrayNode represents an array and the ObjectNode represents an object. The context holds a reference to the root node of the config, and that root node will be used as the starting point for the resolving phase of the parsing process. Developers are free to extend the existing node classes. For example, if you want to extend ObjectNode, first create a class that extends ObjectNode, then register it with parser.registerNode () or Node.registerNode () method. For example, running the following snippet

const parser = require ("@pushcorn/hocon-parser");
const ObjectNode = parser.getClass ("nodes.ObjectNode");

class MyObjectNode extends ObjectNode
{
    constructor (parent, context)
    {
        super (parent, context);

        console.log ("MyObjectNode created!");
    }
}

parser.registerNode (MyObjectNode);

parser.parse ({ text: "a: 1" })
    .then (console.log)
    .catch (console.error);

should give you the following output.

MyObjectNode created!
{ a: 1 }

Source

A source is responsible for loading the resource data from a specified location. A resource could be a file on the local disk, a file hosted on a remote server, or the data stored in a database. When a context is initialized, it will check the specified URL and try to determine the protocol of the URL. For example, if the URL is /test.conf, the context will turn it into file:///test.conf. If the URL is test.conf, then it will be resolved to file://<CWD>/test.conf where <CWD> is the absolute path of the current working directory. The parser supports the following protocols by default:

| Protocol | Source Class | Loading Data from... | |-------------|------------------|----------------------| | context: | ContextSource | Context.text | | file: | FileSource | a local file at the given path | | http: | HttpSource | a remote file at the given URL with the http: protocol | | https: | HttpsSource | a remote file at the given URL with the https: protocol | | modulepath: | ModulepathSource | a local file that can be found under the module search paths (module.paths) | | classpath: | ModulepathSource | classpath is an alias of modulepath |

You can define your own source, but the name of the class should be in the format of <Protocol>Source and it should extend SourceAdapter.

const { SourceAdapter, registerComponent } = require ("@pushcorn/hocon-parser");

class TestSource extends SourceAdapter
{
    async onLoad ()
    {
        // load and return the data
    }
}

registerComponent (TestSource);

Once the source is registered, the context will use the source to load the data when the URL is test://....

Builder

A builder is responsible for building the root node for the context. When the resolve method of Context is called, it will ask the root node to perform the resolution by calling Node.resolve (). The context will try to find the proper builder to build the root node. For example, if the file extension is .conf or .json, it will use the ConfigBuilder. If the data source returns a Buffer or a JavaScript object, then it will use the ContentBuilder, which will return the buffer or the object when the node resolves. If the file extension is .js, then it will choose ScriptBuilder, which will evaluate the loaded script in the new or current (or this) context.

To define your own builder, simply extend BuilderAdapter and implement the build method. Remember to call this.context.createNode () to create the root node.

const { BuilderAdapter, registerComponent } = require ("@pushcorn/hocon-parser");

class TestBuilder extends BuilderAdapter
{
    async build (data)
    {
        ...
        this.context.createNode (<NodeClass>)
    }
}

registerComponent (TestBuilder);

Transform

As the name suggests, a transform is used to convert or post-process the loaded data or value. Below is an example of using the HashTransform to get the md5 checksum of an image file.

image-hash = @image.png | hash { algorithm = md5 }

If an input value is not provided, an empty string will be used as the input. To get the md5 hash of the empty string, you could use the following config.

empty-string-hash = | hash { algorithm = md5 }

To define your own transform is very easy too. You just need to extend the TransformAdapter class and implement the onApply method. If your transform takes any option, simply add a defaults property to the transform class.

const { TransformAdapter, registerComponent } = require ("@pushcorn/hocon-parser");

class TestTransform extends TransformAdapter
{
    async onApply (resolution)
    {
        // Do something with the resolution and return the result.
        // To get the value of opt1, use "this.$.opt1".
    }
}

TestTransform.defaults =
{
    opt1: <default_value_1>,
    opt2: <default_value_2>
};


registerComponent (TestTransform);

License

@pushcorn/hocon-parser is released under the ISC license.

ISC License (ISC) Copyright © 2019 Joseph Tzeng

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.