Current section

Files

Jump to
toml_elixir README.md
Raw

README.md

# TomlElixir

[![Build Status](https://github.com/nikolauska/toml_elixir/actions/workflows/elixir.yml/badge.svg)](https://github.com/nikolauska/toml_elixir/actions)
[![Coverage Status](https://coveralls.io/repos/github/nikolauska/toml_elixir/badge.svg?branch=master)](https://coveralls.io/github/nikolauska/toml_elixir?branch=master)
[![Hex version](https://img.shields.io/hexpm/v/toml_elixir.svg)](https://hex.pm/packages/toml_elixir)

**TomlElixir** is a modern, high-performance [TOML](https://github.com/toml-lang/toml) parser and encoder for Elixir. It is designed to be fully compliant with the TOML specification while providing an idiomatic Elixir experience.

## Features

- **Full Specification Support**: Supports both TOML **1.0.0** and **1.1.0** specifications.
- **Native Elixir Types**: Decodes TOML directly into native Elixir types (`DateTime`, `NaiveDateTime`, `Date`, `Time`, `Float`, `Integer`, `Boolean`, `String`, `Map`, `List`).
- **Bidirectional**: Full support for both decoding (TOML to Map) and encoding (Map to TOML).
- **Strict Parsing**: Comprehensive error messages for invalid TOML documents.
- **Spec Selection**: Allows you to choose which TOML version to follow during decoding.
- **Struct Support**: Use the `TomlElixir.Encoder` protocol to encode custom structs via `@derive`.
- **Format-Preserving Editing**: `TomlElixir.Document` changes values in a TOML file while keeping its comments, whitespace, and key order.

## Installation

Add `toml_elixir` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:toml_elixir, "~> 3.3"}
  ]
end
```

## Usage

### Decoding TOML

Convert a TOML string into an Elixir Map.

```elixir
toml_string = """
title = "TOML Example"

[database]
enabled = true
ports = [ 8000, 8001, 8002 ]
temp_targets = { cpu = 79.5, case = 72.0 }
"""

# Simple decoding
{:ok, data} = TomlElixir.decode(toml_string)
data = TomlElixir.decode!(toml_string)

# Specify TOML version (default is :"1.1.0")
TomlElixir.decode(toml_string, spec: :"1.0.0")
```

Keys are strings by default. Use `keys: :atoms!` to convert them to atoms that already
exist; a key without one fails decoding, so documents cannot create new atoms. For tighter
control, pass a function such as an explicit mapping:

```elixir
TomlElixir.decode("[server]\nport = 80", keys: :atoms!)
#=> {:ok, %{server: %{port: 80}}}

mapping = %{"server" => :server, "port" => :port}
TomlElixir.decode("[server]\nport = 80", keys: &Map.fetch!(mapping, &1))
#=> {:ok, %{server: %{port: 80}}}
```

Use `:transform` to convert or validate values as they are decoded. The callback gets the
key path and the decoded value, children before their parent table, and returns the value
to use. Raise to reject a value:

```elixir
transform = fn
  ["server", "port"], port when port not in 1..65_535 -> raise ArgumentError, "port out of range"
  ["server"], %{"host" => host, "port" => port} -> %MyApp.Server{host: host, port: port}
  _path, value -> value
end

TomlElixir.decode("[server]\nhost = \"localhost\"\nport = 8080", transform: transform)
#=> {:ok, %{"server" => %MyApp.Server{host: "localhost", port: 8080}}}
```

### Encoding to TOML

Convert an Elixir Map back into a valid TOML string.

```elixir
data = %{
  "title" => "TOML Example",
  "database" => %{
    "enabled" => true,
    "ports" => [8000, 8001, 8002]
  }
}

{:ok, toml_string} = TomlElixir.encode(data)
toml_string = TomlElixir.encode!(data)

# Optional layout controls; the default output is unchanged
TomlElixir.encode!(data, multiline_arrays: true, inline_tables: [["database"]])

# Iodata for APIs that accept it, such as File.write/3
{:ok, iodata} = TomlElixir.encode_to_iodata(data)
File.write!("config.toml", TomlElixir.encode_to_iodata!(data))
```

### Editing TOML Without Losing Formatting

`TomlElixir.Document` edits a document in place. Untouched parts render back exactly as
written, including comments, key order, and value formats such as hex integers. New keys
and tables are placed next to related content. See the `TomlElixir.Document` docs for
the placement rules and current limits.

```elixir
toml = """
# Server settings
[server]
port = 0x1F90 # hex on purpose
"""

doc =
  toml
  |> TomlElixir.Document.parse!()
  |> TomlElixir.Document.put!(["server", "port"], 9090)
  |> TomlElixir.Document.put!(["server", "host"], "0.0.0.0")

TomlElixir.Document.to_string(doc)
#=> "# Server settings\n[server]\nport = 9090 # hex on purpose\nhost = \"0.0.0.0\"\n"
```

### Errors

Decoding errors include where the parser detected the problem. `line` and `column` are
1-based, with columns counted in Unicode codepoints; `offset` is a 0-based byte offset
into the input.

```elixir
{:error, %TomlElixir.Parser.Error{reason: "Invalid value nope", line: 2, column: 5, offset: 10} = error} =
  TomlElixir.decode("a = 1\nb = nope")

Exception.message(error)
#=> "Invalid value nope at line 2, column 5"
```

Encoding errors include the path to the value that could not be encoded:

```elixir
{:error, %TomlElixir.Encoder.Error{path: ["servers", 1, "port"]} = error} =
  TomlElixir.encode(%{servers: [%{port: 80}, %{port: nil}]})

Exception.message(error)
#=> "servers[1].port: nil is not supported in TOML"
```

### Encoding Structs

You can use the `TomlElixir.Encoder` protocol to allow your structs to be encoded as TOML.

```elixir
defmodule User do
  @derive TomlElixir.Encoder
  defstruct [:name, :age]
end

user = %User{name: "Alice", age: 30}
toml_string = TomlElixir.encode!(%{user: user})
# [user]
# age = 30
# name = "Alice"
```

You can also choose which struct keys are encoded:

```elixir
defmodule User do
  @derive {TomlElixir.Encoder, only: [:name]}
  defstruct [:name, :age, :password]
end

defmodule PublicUser do
  @derive {TomlElixir.Encoder, except: [:password]}
  defstruct [:name, :age, :password]
end
```

Prefer `:only` to avoid leaking new fields added to a struct in the future.

If you do not own the struct module, you can derive the protocol externally:

```elixir
Protocol.derive(TomlElixir.Encoder, NameOfTheStruct, only: [:public_field])
Protocol.derive(TomlElixir.Encoder, NameOfTheStruct, except: [:private_field])
Protocol.derive(TomlElixir.Encoder, NameOfTheStruct)
```

## Type Mapping

| TOML Type | Elixir Type |
| :--- | :--- |
| String | `String.t()` |
| Integer | `Integer.t()` |
| Float | `Float.t()` (plus `:infinity`, `:neg_infinity`, `:nan`) |
| Boolean | `boolean()` |
| Offset Date-Time | `DateTime.t()` |
| Local Date-Time | `NaiveDateTime.t()` |
| Local Date | `Date.t()` |
| Local Time | `Time.t()` |
| Array | `List.t()` |
| Table | `Map.t()` |
| Inline Table | `Map.t()` |

## Contribution

Contributions are welcome! If you find a bug or want to suggest a feature, please open an issue or submit a pull request. Make sure all tests pass before submitting.

```bash
# Run tests
mix test

# Run coverage report
mix coveralls

# Run decode, encode, and document editing benchmarks separately (time, memory, and reductions)
mix run bench/decode.exs
mix run bench/encode.exs
mix run bench/document.exs
```

## License

TomlElixir is released under the [MIT License](LICENSE.md).