Unicorn is able to generate code in golang that contains a struct to parse configuration data into as well as functions that will open configuration (JSON or YAML) files and parse their contents into the configuration struct provided.
To instruct Unicorn to generate such a file, simply pass the -go <filename>
flag.
The generated Go source code file will be prescribed the package config
and will contain:
- A
Configuration
struct - A
LoadConfigJson(string) (Configuration, error)
function that accepts a JSON file name - A
LoadConfigYaml(string) (Configuration, error)
function that accepts a YAML file name
As demonstrated in program.go
, after placing a generated config.go
file in a config config/
directory, we can simply import the config
package and parse a generated config/config.json
file into an instance of config.Configuration
.
The configuration data in this example was generated by running Unicorn on the Fig file
config/test.fig
using the command (from this directory)
../../unicorn -json config/out.json -yaml config/out.yaml -go config/config.go config/test.fig
The Fig file in question defines some information about a hypothetical API that our fictional
program.go
service might want to invoke, as well as some information about the service itself
(such as the port and address it should run on).
Due to the way variables are handled in the Unicorn interpreter and some limitations of Go's type system,
generated Configuration
structs cannot be typed as accurately as one might like. Here's a breakdown
of what your Configuraion
struct is likely to contain:
- Integers are always typed
int64
- Floats are always typed
float64
- Strings are always typed
string
- Booleans are always typed
bool
- Lists are typed
[]interface{}
- Maps are typed
map[string]interface{}
Since lists and maps are generic containers in Fig, Unicorn has to rely on the interface{}
type
to be able to contain any kind of value. That mean that, in order to operate on the contents
of a list or a value in a map, you must type cast interface{}
to one of the four base types used.
First approach:
If you know the type ahead of time (i.e. it's clear from the Fig code), you can just directly cast a value. For example:
DoSomething(configuration.SomeList[0].(bool))
Second approach:
Determine the type of the value before casting it.
value := configuration.SomeMap["someKey"]
switch value.(type) {
case int64:
HandleInt(value.(int64))
case float64:
HandleFloat(value.(float64))
default:
panic("Oh noes! A value doesn't have a numeric type!")
}