otoml

TOML parsing, manipulation, and pretty-printing library (1.0.0-compliant)
README

A TOML parsing and manipulation library for OCaml.

In short:

  • TOML 1.0-compliant.

  • Transparent (no abstract types).

  • Uses immutable data structures.

  • Easy access to deeply nested values.

  • Preserves the order of fields in tables.

  • Preserves original syntax variant (e.g. inline vs normal table) when parsing and printing.

  • Flexible pretty-printing options.

  • Does not force a calendar or bignum library dependency on you (you can plug your own into the functor).

Goals

The main goal for writing another TOML library is to provide a library for manipulating TOML files, not just reading them.

TOML is designed as a configuration file format.
It's not just a serialization format for machines to talk to one another.
A lot of time it's written, edited, and read by humans.

That is why TOML supports comments and multiple ways to write the same data.

Ideally, when a program reads a TOML file and writes it back, it should be able to echo it back and respect
user's choice of using inline records vs sections (i.e. section = {...} vs [section]) and so on.

OTOML preserves that information and makes it available to the user.

It also offers a convenient interface for accessing and modifying values in deeply nested tables.

Example

utop # #require "otoml";;

(* Parse a TOML string. *)

utop # let t = Otoml.Parser.from_string "
[settings]
  [settings.basic]
    crash_randomly = true
" ;;
val t : Otoml.t =
  Otoml.TomlTable
   [("settings",
     Otoml.TomlTable
      [("basic", Otoml.TomlTable [("crash_randomly", Otoml.TomlBoolean true)])])]

(* Look up a deeply nested value with a known type. *)
utop # Otoml.find t Otoml.get_boolean ["settings"; "basic"; "crash_randomly"] ;;
- : bool = true

(* Update a deeply nested value. *)
utop # let t = Otoml.update t ["settings"; "basic"; "crash_randomly"] (Some (Otoml.integer 0)) ;;
val t : Otoml.t =
  Otoml.TomlTable
   [("settings",
     Otoml.TomlTable
      [("basic", Otoml.TomlTable [("crash_randomly", Otoml.TomlInteger 0)])])]

(* Look up a value and convert it to desired type (if possible). *)
utop # Otoml.find t (Otoml.get_boolean ~strict:false) ["settings"; "basic"; "crash_randomly"] ;;
- : bool = false

(* There's a pretty-printer, too! *)
utop # let t = Otoml.Parser.from_string "[foo] \n [foo.bar] \n baz = {quux = false} \n xyzzy = [ ] \n" |>
  Otoml.Printer.to_channel ~indent_width:4 ~indent_subtables:true ~collapse_tables:true stdout ;;

[foo.bar]
    baz = {quux = false}
    xyzzy = []

val t : unit = ()

Bring your own dependencies

The TOML specification requires support for datetime values and arbitrary large numbers.
For a language that uses machine types and doesn't have datetime support in the standrad library,
it means that implementations have to make a choice whether to be light on dependencies and easy to use
or be standard-compliant.

OTOML solves that problem with OCaml functors.

The default implementation is provided for convenience: it represents integer and floating point numbers
with OCaml's native int and float types, and stores date/time values as strings
that you can parse with your favorite calendar library.

However, it's not hardcoded but built with a functor.
This is how you could assemble the default implementation yourself.

module DefaultToml = Otoml.Base.Make (Otoml.Base.OCamlNumber) (Otoml.Base.StringDate)

Thus you can replace any of the modules or all of them with your own.
For example, this is how you can use zarith
and decimal for big numbers,
but keep simple string dates:

module BigNumber = struct
  type int = Z.t
  type float = Decimal.t

  let int_of_string = Z.of_string
  let int_to_string = Z.to_string
  let int_of_boolean b = if b then Z.one else Z.zero
  let int_to_boolean n = (n <> Z.zero)

  (* Can't just reuse Decimal.to/of_string because their optional arguments
     would cause a signature mismatch. *)
  let float_of_string s = Decimal.of_string s

  (* Decimal.to_string uses "NaN" spelling
     while TOML requires all special float values to be lowercase. *)
  let float_to_string x = Decimal.to_string x |> String.lowercase_ascii
  let float_of_boolean b = if b then Decimal.one else Decimal.zero
  let float_to_boolean x = (x <> Decimal.zero)

  let float_of_int = Decimal.of_bigint
  let int_of_float = Decimal.to_bigint
end

module MyToml = Otoml.Base.Make (BigNumber) (Otoml.Base.StringDate)

Deviations from the TOML 1.0 specification

The default implementation is not fully compliant with the standard. These are the deviations:

The default implementation uses OCaml's native integer type, which is 63-bit on 64-bit architectures and 31-bit on 32-bit ones.

Numbers greater than maximum representable values will cause generic parse errors
(string ... does not represent a valid integer/floating point number).
More precise error message for that case may be added in the future.

The default implementation does not interpret datetime values at all,
only checks them for superficial validity and returns as strings.

For example, "1993-09-947" is considered invalid (as expected), but 1993-02-29 is valid despite the fact that 1993 wasn't a leap year.

Thus, actual precision depends on the library you use to parse those date strings or plug into the functor.

Users

  • soupault (static website framework based on HTML element tree rewriting)

  • fromager (ocamlformat frontend)

  • camyll (static website generator with literate Agda support)

  • dirsift (directory search utility)

  • lab (GitLab CLI)

Install
Published
28 Dec 2021
Sources
1.0.1.tar.gz
md5=28d9e94adbefc256d6a5b0786194d8fc
sha512=f78f325494219d7eb562c7cfe98d5999244a769b1f8d18a5fb3d8c60384d07cee029b083977143a7385cc14dde30dc7af0ce9e4aa2efdd62df42e256538cf690
Dependencies
odoc
with-doc
ounit2
with-test
uutf
>= "1.0.0"
dune
>= "2.0.0"
menhirLib
>= "20200525"
ocaml
>= "4.08.0"
Reverse Dependencies
camyll
>= "0.4.0"
dirsift
>= "0.0.4"
fromager
>= "0.5.0"
soupault
>= "3.2.1"