Fig: Simple and Extensible Configuration
#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:
Variables: Don’t repeat yourself! Name and reuse parts of the configuration.
Input: Include an environment of input variables to generalize the configuration to different contexts.
Merge: Create small, reusable objects; then combine them into more complex parts.
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:
Numbers like 1.5 are represented as number?.
Strings like "hello" are represented as string?. Fig supports the standard escape characters.
Booleans like true and false are represented as boolean?.
Lists like [1, 2, 3] are represented as list?.
Objects like {"hello": 5} are represented as hash?.
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}}
{"key1": {"key2": 2, "key3": 3}}
Merge fails in the following three cases:
e1 and e2 are different types.
e1 and e2 are both non-objects but are not equal?.
e1 and e2 are objects that share a key where the values fail one of the two previous criteria.
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
environment : (hash/c string? any/c)
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)