Read the reference documentation.

Below is a guided tour of the library through examples.

Defining options

Let's create the host and port options with default values "localhost" and 80:

# let host = Ocf.string ~doc: "Host name" "localhost";;
val host : string Ocf.conf_option = <abstr>
# let port = Ocf.int ~doc: "Port number" 80;;
val port : int Ocf.conf_option = <abstr>

The ~doc parameter is used to give some description of the option. It will be used later. Note that Ocf.string and Ocf.int return typed options, here with types string Ocf.conf_option and int Ocf.conf_option. These functions are specialisations of the Ocf.option function. Other such functions are provided (see here).

Option values are modified in-place; the current value is retrieved using Ocf.get:

# Ocf.get host;;
- : string = "localhost"

Now we create a group server_options. A group is a set of groups and options, each having a path (a list of strings) from the root of the group. Each part of a path will be mapped to a field in JSON objects.

# let server_options =
  let g = Ocf.add Ocf.group ["host"] host in
  Ocf.add g ["port"] port ;;
val server_options : [ `Open ] Ocf.group = <abstr>

Note that Ocf.group is not a function. Adding things to the group returns a new one. Here we built a new group by adding our option host to the path ["host"], and port to the path ["port"].

Input and output

We can already load values for our options from a JSON string (the library also provide functions to read from and write to JSON files). Let's define a JSON string and read it to get values:

# let () = Ocf.from_string server_options
  {| { host: "foo.bar.net", port: 8080 } |};;

Let's see the values of our options:

# Ocf.get host ;;
- : string = "foo.bar.net"
# Ocf.get port ;;
- : int = 8080

We can read another JSON string. Values defined in this JSON will replace the previous values of our options, but of course only when values are given.

# let () = Ocf.from_string server_options
  {| { host: "my.host.org", dummy: 42 } |} ;;
# Ocf.get host (* changed *);;
- : string = "my.host.org"
# Ocf.get port (* unchanged *) ;;
- : int = 8080

Note that the dummy field in the JSON object is ignored, as it does not correspond to a defined option.

We can get a JSON representation of our group of options:

# print_endline (Ocf.to_string server_options);;
{
  "port": "Port number",
  "port": 8080,
  "host": "Host name",
  "host": "my.host.org"
}
- : unit = ()

Since JSON format does not allow comments, Ocf uses this hack to show documentation strings in the JSON object. An option having a documentation string appears twice: first with the associated documentation string, then associated with the option value of the correct type. After reading the JSON object, only the second association is kept.

Output functions can be called with ~with_doc: false to prevent the output of the documentation string:

# print_endline (Ocf.to_string ~with_doc: false server_options);;
{ "port": 8080, "host": "my.host.org" }
- : unit = ()
Groups and paths

Let's define three additional options, db_name, db_host and db_port:

# let db_name = Ocf.string "mydb";;
val db_name : string Ocf.conf_option = <abstr>
# let db_host = Ocf.string "localhost";;
val db_host : string Ocf.conf_option = <abstr>
# let db_port = Ocf.int 3306;;
val db_port : int Ocf.conf_option = <abstr>

We can put all these options in a db_options group and see the JSON output:

# let db_options =
  let g = Ocf.add Ocf.group ["name"] db_name in
  let g = Ocf.add g ["host"] db_host in
  Ocf.add g ["port"] db_port;;
val db_options : [ `Open ] Ocf.group = <abstr>
# print_endline (Ocf.to_string db_options);;
{ "port": 3306, "name": "mydb", "host": "localhost" }
- : unit = ()

Now we can put server_options and db_options in a new app_options group (with Ocf.add_group), each one being assigned a path from the root of this new group:

# let app_options =
  let g = Ocf.add_group Ocf.group ["server"] server_options in
  Ocf.add_group g ["database"] db_options;;
val app_options : [ `Open ] Ocf.group = <abstr>
# print_endline (Ocf.to_string app_options);;
{
  "server": {
    "port": "Port number",
    "port": 8080,
    "host": "Host name",
    "host": "my.host.org"
  },
  "database": { "port": 3306, "name": "mydb", "host": "localhost" }
}
- : unit = ()

The same result could have been obtained by adding all our options with full paths to a single group, here app_options2:

# let app_options2 =
  let g = Ocf.add Ocf.group ["server" ; "host"] host in
  let g = Ocf.add g ["server" ; "port"] port in
  let g = Ocf.add g ["database" ; "name"] db_name in
  let g = Ocf.add g ["database" ; "host"] db_host in
  Ocf.add g ["database" ; "port"] db_port;;
val app_options2 : [ `Open ] Ocf.group = <abstr>
# print_endline (Ocf.to_string app_options2);;
{
  "server": {
    "port": "Port number",
    "port": 8080,
    "host": "Host name",
    "host": "my.host.org"
  },
  "database": { "port": 3306, "name": "mydb", "host": "localhost" }
}
- : unit = ()

Using groups allows to define options in a modular way. Modules defining a group of options do not need to known the full path of the options in the configuration file.

Wrappers

A wrapper ('a Ocf.wrapper) is a pair of functions to convert values of type 'a to JSON ('a -> Yojson.Safe.json) and from JSON (?def: 'a -> Yojson.Safe.json -> 'a). The ?def parameter will be given the current value of the option the wrapper is associated to, if any. Default wrappers do not use this parameter, but it is used in record wrappers.

An option is built using such a wrapper, like Ocf.Wrapper.string or Ocf.Wrapper.int in the example above (used by Ocf.string and Ocf.int convenient functions). Other wrappers and wrapper combinators are available in the Wrapper module.

For example, we can define a wrapper for lists of string * int pairs and use this wrapper to create an option:

# let wrapper = Ocf.Wrapper.(list (pair string int));;
val wrapper : (string * int) list Ocf.Wrapper.t =
  {Ocf.Wrapper.to_json = <fun>; from_json = <fun>}
# let pairs = Ocf.option wrapper [ ("hello", 1) ; ("world", 2)];;
val pairs : (string * int) list Ocf.conf_option = <abstr>

Let's see how this will be stored in JSON (and so the expected JSON to give values for this option):

# let () =
  let group = Ocf.add Ocf.group ["dummy"] pairs in
  print_endline (Ocf.to_string group);;
{ "dummy": [ ("hello", 1), ("world", 2) ] }

A wrapper can be created with function Ocf.Wrapper.make by giving your own 'a -> Yojson.Safe.json and ?def: 'a -> Yojson.Safe.json -> 'a functions.

Wrapping records with a ppx

Since it is often useful to define record types to represent a configuration and to be able to load and store such a configuration from and to a file, one way want to create a wrapper from a record type.

This can be done by creating special functions to map values of such record type to and from JSON and use these functions to make a wrapper but this is repetitive. Fortunately, Ocf comes with a ppx processor, in package ocf_ppx to automate the generation of wrappers from record types.

Here is an example. Let's change our previous example to define a server configuration record type as:

type server = { host: string; port: int };;

To enable code generation, we add some annotations:

# type server =
  { host: string [@ocf Ocf.Wrapper.string, "localhost"] [@ocf.label "hostname"] ;
    port: int [@ocf Ocf.Wrapper.int, 8080] ;
  } [@@ocf];;
type server = { host : string; port : int; }
val default_server : server = {host = "localhost"; port = 8080}
val server_wrapper : server Ocf.Wrapper.t =
  {Ocf.Wrapper.to_json = <fun>; from_json = <fun>}

To trigger the generation, the type definition must have a [@@ocf] attribute. Each field can have a [@ocf ] attribute (on the type) with a pair of expressions. The first part of the pair is the wrapper, the second part is the default value for the field.

Then, two values are generated: default_server is a value of type server with the given default value for each field. server_wrapper is the wrapper to load and store values of type server from and to JSON. When a field is not present in the JSON representation, the default value will be used, to have a value for each field.

By default, a field in the JSON object has the same name as the corresponding field of the record type. The [@ocf.label] attribute on the field type can be used to specify another label to use in the JSON object, such as "hostname" in the example above.

Let's define an option with our new wrapper and print its JSON representation:

# let server = Ocf.option server_wrapper default_server ;;
val server : server Ocf.conf_option = <abstr>
# let group = Ocf.add Ocf.group ["server"] server ;;
val group : [ `Open ] Ocf.group = <abstr>
# let () = print_endline (Ocf.to_string group);;
{ "server": { "hostname": "localhost", "port": 8080 } }

Here is an example of setting the value of our server option:

# Ocf.get server ;;
- : server = {host = "localhost"; port = 8080}
# Ocf.from_string group {| { "server" : { "hostname": "my.host.com" } } |} ;;
- : unit = ()
# Ocf.get server ;;
- : server = {host = "my.host.com"; port = 8080}

Note that giving only some fields of the record in a JSON object will change only the corresponding fields of the record value (but a new record value will be allocated, though):

# Ocf.from_string group {| {"server" : { "port": 9000 } } |};;
- : unit = ()
# Ocf.get server;;
- : server = {host = "my.host.com"; port = 9000}
Parametrized record types

One can define parametrized record types. In this case, the generated default value and the wrapper for this type will have parameters:

# type ('a, 'b) entry = {
  key: 'a ; (* wrapper and default value will be parameters *)
  value: 'b list [@ocf Ocf.Wrapper.list, [] ] ;
           (* a wrapper will be expected as parameter for 'b, but default value []
              is already provided *)
  value2: string option [@ocf.wrapper Ocf.Wrapper.option Ocf.Wrapper.string] ;
           (* a wrapper is provided, but no default value *)
  } [@@ocf];;
type ('a, 'b) entry = { key : 'a; value : 'b list; value2 : string option; }
val default_entry : key:'a -> value2:string option -> ('a, 'b) entry = <fun>
val entry_wrapper :
  key:'a ->
  value2:string option ->
  'a Ocf.Wrapper.t -> 'b Ocf.Wrapper.t -> ('a, 'b) entry Ocf.Wrapper.t =
  <fun>