Current section
Files
Jump to
Current section
Files
lib/toml_elixir.ex
defmodule TomlElixir do
@moduledoc """
# TomlElixir
[TOML](https://github.com/toml-lang/toml) parser for elixir.
## Installation
The package can be installed by adding `toml_elixir` to your list of
dependencies in `mix.exs`:
```elixir
def deps do
[{:toml_elixir, "~> 1.1.0"}]
end
```
## Usage
TomlElixir is used by calling parse functions
* `TomlElixir.parse/2`
* `TomlElixir.parse!/2`
* `TomlElixir.parse_file/2`
* `TomlElixir.parse_file!/2`
"""
@typedoc """
Toml value is a tuple with type and actual value
"""
@type toml_value :: {:string, binary} |
{:datetime, tuple} |
{:number, number} |
{:boolean, boolean}
@typedoc """
Toml ident is same as value tuple but this means identifier or key
"""
@type toml_ident :: {:identifier, binary}
@typedoc """
Toml key value means tuple with toml identifier and value
## Example
Toml:
```toml
key = value
```
Tuple:
```
{{:identifier, "key"}, {:string, "value"}}
```
Map:
```
%{"key" => "val"}
```
"""
@type toml_key_val :: {toml_ident, toml_value}
@typedoc """
Toml group means list of values
First list is list of identifiers which point to place in map
## Example
Toml:
```toml
[key]
example = ["value", true]
```
Tuple:
```
{:group,
[{:identifier, "key"}, {:identifier, "example"}],
[{:string, "value"}, {:boolean, true}]}
```
Map:
```
%{
"key" => %{
"example" => ["value", true]
}
}
```
"""
@type toml_group :: {:group, [toml_ident], [toml_key_val]}
@typedoc """
Multi is same as group but with difference that it's a list of maps
## Example
Toml:
```toml
[[key]]
example1 = val1
[[key]]
example2 = val2
```
Tuple:
```
[
{:multi,
[{:identifier, "key"}, {:identifier, "example1"}],
[{:string, "val1"}]},
{:multi,
[{:identifier, "key"}, {:identifier, "example2"}],
[{:string, "val2"}]},
]
```
Map:
```
%{
"key" => [
%{"example1" => "val1"},
%{"example2" => "val2"},
]
}
```
"""
@type toml_multi :: {:multi, [toml_ident], [toml_key_val]}
@typedoc """
Toml return is just list of any toml types
"""
@type toml_return :: [toml_key_val | toml_multi | toml_group] | []
@type options :: [to_map: boolean]
@type result :: map | toml_return
@doc """
Parse toml string to map or return toml tuple list.
## Example
```
TomlElixir.parse("toml = true")
```
"""
@spec parse(binary, options) :: {:ok, result} | {:error, String.t}
def parse(str, opts \\ []) when is_binary(str) do
with {:ok, tokens} <- lexer(str),
{:ok, list} <- parser(tokens)
do
if to_map?(opts) do
{:ok, to_map(list)}
else
{:ok, list}
end
end
end
@doc """
Same as `parse/2`, but raises error on failure
## Example
```
TomlElixir.parse!("toml = true")
```
"""
@spec parse!(binary, options) :: result
def parse!(str, opts \\ []) when is_binary(str) do
case parse(str, opts) do
{:ok, map} -> map
{:error, err} -> raise err
end
end
@doc """
Parse toml file, uses same options as `parse/2`
## Example
```
TomlElixir.parse_file("path/to/example.toml")
```
"""
@spec parse_file(binary, options) :: {:ok, result} | {:error, String.t}
def parse_file(path, opts \\ []) do
with {:ok, str} <- File.read(path)
do
parse(str, opts)
end
end
@doc """
Same as `parse_file/2`, but raises error on failure
## Example
```
TomlElixir.parse_file!("path/to/example.toml")
```
"""
@spec parse_file!(binary, options) :: result
def parse_file!(path, opts \\ []) do
case parse_file(path, opts) do
{:ok, toml} -> toml
{:error, err} -> raise err
end
end
# Check if we skip parsing to map
@spec to_map?(keyword) :: boolean
defp to_map?(opts) do
cond do
Keyword.get(opts, :no_parse) == true ->
IO.puts("#{__MODULE__}: no_parse option is deprecated, " <>
"please use new to_map: false option")
false
Keyword.get(opts, :to_map) == false -> false
true -> true
end
end
# Tokenize toml file
@spec lexer(binary) :: {:ok, list} | {:error, String.t}
defp lexer(str) when is_binary(str) do
str
|> to_charlist()
|> :toml_lexer.string()
|> erl_result_parse()
end
# Parse tokens to tuples
@spec parser(list) :: {:ok, list} | {:error, String.t}
defp parser(tokens) when is_list(tokens) do
tokens
|> :toml_parser.parse()
|> erl_result_parse()
end
# Parses errors from lexer or parser
@spec erl_result_parse({:ok, [any], any} | {:ok, [any]} |
{:error, {number, any, binary}} |
{:error, {number, any, {atom, binary}, any}}) ::
{:ok, [any]} | {:error, String.t}
defp erl_result_parse({:ok, tokens, _}),
do: {:ok, tokens}
defp erl_result_parse({:ok, list}),
do: {:ok, list}
defp erl_result_parse({:error, {line, _, err}}),
do: {:error, "Error on line #{line}: #{err}"}
defp erl_result_parse({:error, {line, _, {err, msg}}, _}),
do: {:error, "Error on line #{line}: #{err} #{msg}"}
# Turn toml tuple list to map
@spec to_map(toml_return) :: map
@spec to_map(toml_return, [] | [any] | map) :: map
defp to_map(val),
do: to_map(val, %{})
defp to_map(val, []),
do: [to_map(val, %{})]
defp to_map(val, list) when is_list(list),
do: List.update_at(list, -1, &(to_map(val, &1)))
defp to_map([{:group, idents, values} | tail], map),
do: to_map(tail, group(idents, values, map))
defp to_map([{:multi, idents, values} | tail], map),
do: to_map(tail, multi(idents, values, map))
defp to_map([{{:identifier, key}, values} | tail], map) when is_list(values),
do: to_map(tail, put(map, key, value(values)))
defp to_map([{{:identifier, key}, val} | tail], map),
do: to_map(tail, put(map, key, value(val)))
defp to_map([], map),
do: map
# Turn group tuple to map
@spec group([toml_ident], [toml_key_val], [any] | map) :: map | [any]
defp group(idents, values, []),
do: [group(idents, values, %{})]
defp group(idents, values, list) when is_list(list),
do: List.update_at(list, -1, &group(idents, values, &1))
defp group([{:identifier, key} | tail], values, map),
do: put(map, key, group(tail, values, get(map, key, %{})))
defp group([], values, map),
do: to_map(values, map)
# Turn multi tuple to map
@spec multi([toml_ident], [toml_key_val], map) :: map
defp multi([{:identifier, key} | []], values, map),
do: put(map, key, to_map(values, insert_end(map, key, %{})))
defp multi([{:identifier, key} | tail], values, map),
do: put(map, key, multi(tail, values, get(map, key, %{})))
# Parse value from toml value tuple
@spec value(toml_value | [toml_value]) :: any
defp value([]), do: []
defp value([head | tail]), do: [value(head) | value(tail)]
defp value({:string, val}), do: "#{val}"
defp value({:datetime, val}), do: val
defp value({:number, val}), do: val
defp value({:boolean, val}), do: val
# Add value to end of the list
@spec insert_end(map, binary, any) :: [map]
defp insert_end(map, key, value),
do: List.insert_at(get(map, key, []), -1, value)
# Get value from map
@spec get(map, binary, any) :: any
defp get(map, key, default),
do: Map.get(map, "#{key}", default)
# Put value to map
@spec put(map, binary, any) :: map
defp put(map, key, value),
do: Map.put(map, "#{key}", value)
end