Home

Awesome

fern

CircleCI

Clojars Project

Fern is a simple but useful language for data. While you can use Fern to read any kind of trusted data, Fern has semantics that make it extremely useful for configuration data.

Usage

Fern data is stored in the form of EDN, specifically an EDN map with symbols for keys. Here, for example, is a simple Fern file:

{host "localhost"
 timeout 4000}

The thing that makes Fern Fern is that it provides a way for one part of a Fern file to reference another. The syntax for recursively evaluating a symbol in Fern is identical to the Clojure dereference syntax. For example, let's imagine that you need to configure three servers. Each server listens on a different port, but all happen to be on the same host and use the timeout value. In this case, the Fern configuration might look like:

{host "localhost"
 timeout 4000
 server-1 {:host @host :request-timeout @timeout :port 8000}
 server-2 {:host @host :request-timeout @timeout :port 8010}
 server-3 {:host @host :request-timeout @timeout :port 8020}}

Note how the :host values are all set to @host which is a reference to host which ultimately resolves to "localhost".

Specifically the rules of Fern evaluation are:

API

Fern provides two levels of API. The core Fern API is defined in the fern namespace, while the fern.easy namespace provides a number of convenience functions that make reading Fern files relatively painless. For example, fern.easy/file->environment takes a path and reads the fern file at that path and returns the Fern environment:

(require 'fern.easy)

(def e (fern.easy/file->environment "example.fern"))

Once you have a fern environment, you can use fern/evaluate to pull values out of it:

(require 'fern)

(def server-1-map (fern/evaluate e 'server-1)

Plugins

Since Fern allows you to add custom processing by defining additional methods on the fern/literal multimethod, it also supplies a short cut for loading in new namespaces from the Fern file. To use the shortcut, you use fern.easy/load-environment in place of fern.easy/file->environment. The fern.easy/load-environment is nearly identical to fern.easy/file->environment: The difference is that it takes a second argument. That second argument should be a symbol in the Fern file whose value is a collection of namespaces to require. For a (slightly contrived) example, imagine we had a Clojure namespace called server-name:

(ns server-name
  (:require [fern :as f]))

(defmethod f/literal :server-name [_ n1 n2]
  (str n1 "." n2 ".com"))

Using the plugins facility we can pull in that namespace and then use the :server-name literal in our file.

{plugins  [server-name]
 host-1 "server"
 host-2 "example"
 timeout 4000

 host     (fern/lit :server-name host-1 host-2)

 server-1 {:host @host :request-timeout @timeout :port 8000}
 server-2 {:host @host :request-timeout @timeout :port 8010}
 server-3 {:host @host :request-timeout @timeout :port 8020}}

Then you can call fern like this:

(require 'fern.easy)

(def e (fern.easy/load-environment "example.fern" 'plugins))

And discover with (fern/evaluate e 'host) that your host is "server.example.com".

Reporting Errors

Fern offers an easy way to print errors. If you get an exception from fern/evaluate, fern.easy/load-environment, or fern.easy/validate!, you can call fern.easy/print-evaluation-exception to print out a nicely formatted message.

License

Copyright © 2017 Cognitect, Inc.

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.