Current section
Files
Jump to
Current section
Files
toml_elixir
README.md
README.md
# TomlElixir
[](https://github.com/nikolauska/toml_elixir/actions)
[](https://coveralls.io/github/nikolauska/toml_elixir?branch=master)
[](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).