Fig:   Simple and Extensible Configuration
1 Introduction
2 The Fig Language
2.1 Literals
2.2 Variables
2.3 Environment and Input
2.4 Conditionals and Equality
2.5 Merge
2.6 Comments and Trailing Commas
2.7 Grammar
3 Using Fig
fig->hash
fig->json
8.12

Fig: Simple and Extensible Configuration🔗ℹ

Micah Cantor

 #lang fig package: fig

1 Introduction🔗ℹ

Fig is a domain-specific language for composing configuration files. Fig is a super-set of JSON with some additional features to reduce repetition and increase correctness. These features include:

Here’s a configuration for a web service that demonstrates these features, saved in a file named config.fig:

#lang fig
 
let user-info = { "username": @username, "email": @email } let server-info = { "base": if @local then "http://localhost:3000" else "https://website.com", "endpoints": ["/cats", "/dogs"] } user-info & server-info

Here we have two objects, user-info and server-info that expect a few input variables (each prefixed with the @ operator) to be provided: a username, an email, and a flag for if the service is running locally. These two objects are merged togetherusing the & operator.

Then, in the following Racket program, we instantiate this Fig configuration by providing the required variables:

#lang racket/base
 
(require "config.fig")
 
(fig->json (hash "username" "cat"
                 "email" "cat@email.com"
                 "local" #t))

By requiring the Fig file, we obtain the fig->json procedure for that configuration. By applying this procedure to a table of inputs, we obtain the following JSON output:

{

  "username": "cat",

  "email": "cat@cat.com"

  "base": "http://localhost:3000",

  "endpoints": ["/cats", "/dogs"]

}

2 The Fig Language🔗ℹ

Fig is designed to be a simple extension of JSON, so most features should be straightforward to understand. However, this section will explain the finer details of the language.

2.1 Literals🔗ℹ

Literals in Fig are the same as those available in JSON, corresponding to the following Racket types:

2.2 Variables🔗ℹ

Variable bindings in Fig can be introduced at the top level:

let hello = "world"

This form directly expands to define. Unlike in Racket, identifiers in Fig must begin with an alphabetic character.

2.3 Environment and Input🔗ℹ

When a Fig program is instantiated, an environment may optionally be provided. When using Fig from a Racket program, the environment is provided to the fig->hash or fig->json procedure:

(require "example.fig")
 
(fig->hash (hash "hello" "world"))

Keys in the environment must be strings, while values can be any Racket type. To reference a key from the environment, prefix the name of the key with @ like @hello.

2.4 Conditionals and Equality🔗ℹ

Fig supports conditionals through the syntax:

if CONDITION then CONSEQUENT else ALTERNATE

This form expands directly to Racket if. Fig also supports an equality operator == that expands to Racket’s equal?:

let hello = "world"

if hello == "world" then "yes!" else "no!"

2.5 Merge🔗ℹ

Fig provides a recursive object merge operator &. Merge is a commutative operator, meaning that for any two expressions e1 and e2:

e1 & e2 == e2 & e1

The simplest case for merge is when e1 and e2 are objects with no shared keys. In this case, the result of e1 & e2 is a new object with the key/value pairs from e1 and e2:

{"key1": 1} & {"key2": 2} == {"key1": 1, "key2": 2}

In the case that e1 and e2 share a common key, Fig will attempt to recursively merge the values of these keys:

{"key1": {"key2": 2}} & {"key1": {"key3": 3}}

evaluates to
{"key1": {"key2": 2, "key3": 3}}

Merge fails in the following three cases:

2.6 Comments and Trailing Commas🔗ℹ

Fig supports line comments beginning with //:

let hello = "world" // this is a comment!

Unlike in JSON, Fig also supports objects to have trailing commas:

{

  "key1": 1,

  "key2: 2, // this comma is optional

}

2.7 Grammar🔗ℹ

The full grammar for Fig in brag BNF notation is as follows:

#lang brag
 
fig-program: [fig-let]* fig-expr
fig-let: /"let" ID /"=" fig-expr
@fig-expr: fig-object | fig-list | fig-merge | fig-apply | fig-equal | fig-cond | fig-env-ref | fig-lit
fig-object: /"{" [fig-kvpair (/"," fig-kvpair)* [/","]?] /"}"
fig-list: /"[" [fig-expr (/"," fig-expr)*] /"]"
fig-merge: fig-expr /"&" fig-expr
fig-apply : /"(" [fig-expr]+ /")"
fig-equal: fig-expr /"==" fig-expr
fig-env-ref: ENVREF
fig-cond: /"if" fig-expr /"then" fig-expr /"else" fig-expr
@fig-lit: ID | STRING | NUMBER | "true" | "false" | "null"
fig-kvpair: STRING /":" fig-expr

3 Using Fig🔗ℹ

Fig is designed to be used from within the Racket ecosystem. Since Fig expands to a Racket module, it can be required from a Racket program. In particular, the module provides two procedures:

procedure

(fig->hash environment)  any/c

  environment : (hash/c string? any/c)
Instantiates a Fig program to a hash table (or just a Racket value, if the Fig program does not produce an object). If provided, the keys in the environment are available in Fig as input variables.

procedure

(fig->json environment)  string?

  environment : (hash/c string? any/c)
Instantiates JSON, represented as a string. Internally, uses the json library on the result of fig->hash. If provided, the keys in the environment are available in Fig as input variables.

Given a Fig program example.fig, we can require it from a Racket program:

#lang racket/base
 
(require "example.fig")
 
(fig->hash)

If more than one Fig program are required in a single Racket program, you can use prefix-in to distinguish their provided procedures:

#lang racket/base
 
(require (prefix-in ex1- "ex1.fig")
         (prefix-in ex2- "ex2.fig"))
 
(ex1-fig->hash)
(ex2-fig->hash)