Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/kowainik/tomland

🏝 Bidirectional TOML serialization
https://github.com/kowainik/tomland

bidirectional configuration hacktoberfest haskell profunctors toml toml-parser

Last synced: about 1 month ago
JSON representation

🏝 Bidirectional TOML serialization

Awesome Lists containing this project

README

        

# tomland

![palm](https://user-images.githubusercontent.com/4276606/51088259-7a777000-176e-11e9-9d76-6be4023c0ac3.png)

[![GitHub CI](https://github.com/kowainik/tomland/workflows/CI/badge.svg)](https://github.com/kowainik/tomland/actions)
[![Hackage](https://img.shields.io/hackage/v/tomland.svg?logo=haskell)](https://hackage.haskell.org/package/tomland)
[![MPL-2.0 license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](https://github.com/kowainik/tomland/blob/main/LICENSE)

> “A library is like an island in the middle of a vast sea of ignorance,
> particularly if the library is very tall and the surrounding area has been
> flooded.”

> ― Lemony Snicket, Horseradish

`tomland` is a Haskell library for _Bidirectional TOML
Serialization_. It provides the composable interface for implementing
[TOML](https://github.com/toml-lang/toml) codecs. If you want to use
TOML as a configuration for your tool or application, you can use
`tomland` to easily convert in both ways between textual TOML
representation and Haskell types.

✍️ `tomland` supports [TOML spec version 0.5.0](https://github.com/toml-lang/toml/wiki#v050-compliant).

The following blog post has more details about the library design and
internal implementation details:

* [`tomland`: Bidirectional TOML Serialization](https://kowainik.github.io/posts/2019-01-14-tomland)

This README contains a basic usage example of the `tomland` library. All code
below can be compiled and run with the following command:

```
cabal run readme
```

## Preamble: imports and language extensions

Since this is a literate haskell file, we need to specify all our language
extensions and imports up front.

```haskell
{-# OPTIONS -Wno-unused-top-binds #-}

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

import Control.Applicative ((<|>))
import Data.Text (Text)
import Data.Time (Day)
import Toml (TomlCodec, (.=))

import qualified Data.Text.IO as TIO
import qualified Toml
```

`tomland` is designed for qualified imports and intended to be imported
as follows:

```haskell ignore
import Toml (TomlCodec, (.=)) -- add 'TomlBiMap' and 'Key' here optionally
import qualified Toml
```

## Data type: parsing and printing

We're going to parse TOML configuration from
[`examples/readme.toml`](examples/readme.toml) file. The configuration
contains the following description of our data:

```toml
server.port = 8080
server.codes = [ 5, 10, 42 ]
server.description = """
This is production server.
Don't touch it!
"""

[mail]
host = "smtp.gmail.com"
send-if-inactive = false

[[user]]
guestId = 42

[[user]]
guestId = 114

[[user]]
login = "Foo Bar"
createdAt = 2020-05-19
```

The above static configuration describes `Settings` for some
server. It has several top-level fields, a table with the name `mail`
and an array of tables with the name `user` that stores list of
different types of users.

We can model such TOML using the following Haskell data types:

```haskell
data Settings = Settings
{ settingsPort :: !Port
, settingsDescription :: !Text
, settingsCodes :: [Int]
, settingsMail :: !Mail
, settingsUsers :: ![User]
}

data Mail = Mail
{ mailHost :: !Host
, mailSendIfInactive :: !Bool
}

data User
= Guest !Integer -- id of guest
| Registered !RegisteredUser -- login and createdAt of registered user

data RegisteredUser = RegisteredUser
{ registeredUserLogin :: !Text
, registeredUserCreatedAt :: !Day
}

newtype Port = Port Int
newtype Host = Host Text
```

Using the `tomland` library, you can write bidirectional converters for these types
with the following guidelines and helper functions:

1. If your fields are some simple primitive types like `Int` or `Text` you can just
use standard codecs like `Toml.int` and `Toml.text`.
2. If you want to parse `newtype`s, use `Toml.diwrap` to wrap parsers for
underlying `newtype` representation.
3. For parsing nested data types, use `Toml.table`. But it requires to specify
this data type as TOML table in the `.toml` file.
4. If you have lists of custom data types, use `Toml.list`. Such lists are
represented as _array of tables_ in TOML. If you have lists of the primitive types
like `Int`, `Bool`, `Double`, `Text` or time types, that you can use
`Toml.arrayOf` and parse arrays of values.
5. If you have sets of custom data types, use `Toml.set` or `Toml.HashSet`. Such
sets are represented as array of tables in TOML.
6. For parsing sum types, use `Toml.dimatch`. This requires writing matching functions
for the constructors of the sum type.
7. `tomland` separates conversion between Haskell types and TOML values from
matching values by keys. Converters between types and values have type
`TomlBiMap` and are named with capital letter started with underscore. Main
type for TOML codecs is called `TomlCodec`. To lift `TomlBiMap` to
`TomlCodec` you need to use `Toml.match` function.

```haskell
settingsCodec :: TomlCodec Settings
settingsCodec = Settings
<$> Toml.diwrap (Toml.int "server.port") .= settingsPort
<*> Toml.text "server.description" .= settingsDescription
<*> Toml.arrayOf Toml._Int "server.codes" .= settingsCodes
<*> Toml.table mailCodec "mail" .= settingsMail
<*> Toml.list userCodec "user" .= settingsUsers

mailCodec :: TomlCodec Mail
mailCodec = Mail
<$> Toml.diwrap (Toml.text "host") .= mailHost
<*> Toml.bool "send-if-inactive" .= mailSendIfInactive

matchGuest :: User -> Maybe Integer
matchGuest = \case
Guest i -> Just i
_ -> Nothing

matchRegistered :: User -> Maybe RegisteredUser
matchRegistered = \case
Registered u -> Just u
_ -> Nothing

userCodec :: TomlCodec User
userCodec =
Toml.dimatch matchGuest Guest (Toml.integer "guestId")
<|> Toml.dimatch matchRegistered Registered registeredUserCodec

registeredUserCodec :: TomlCodec RegisteredUser
registeredUserCodec = RegisteredUser
<$> Toml.text "login" .= registeredUserLogin
<*> Toml.day "createdAt" .= registeredUserCreatedAt
```

And now we are ready to parse our TOML and print the result back to see whether
everything is okay.

```haskell
main :: IO ()
main = do
tomlRes <- Toml.decodeFileEither settingsCodec "examples/readme.toml"
case tomlRes of
Left errs -> TIO.putStrLn $ Toml.prettyTomlDecodeErrors errs
Right settings -> TIO.putStrLn $ Toml.encode settingsCodec settings
```

## Benchmarks and comparison with other libraries

You can find benchmarks of the `tomland` library in the following repository:

* [kowainik/toml-benchmarks](https://github.com/kowainik/toml-benchmarks)

Since `tomland` uses 2-step approach with converting text to
intermediate AST and only then decoding Haskell type from this AST,
benchmarks are also implemented in a way to reflect this difference.

| Library | parse :: Text -> AST | transform :: AST -> Haskell |
|--------------------|----------------------|-----------------------------|
| `tomland` | `305.5 μs` | `1.280 μs` |
| `htoml` | `852.8 μs` | `33.37 μs` |
| `htoml-megaparsec` | `295.0 μs` | `33.62 μs` |
| `toml-parser` | `164.6 μs` | `1.101 μs` |

In addition to the above numbers, `tomland` has several features that
make it unique:

1. `tomland` is the only Haskell library that has pretty-printing.
2. `tomland` is compatible with the latest TOML spec while other libraries are not.
3. `tomland` is bidirectional, which means that your encoding and
decoding are consistent with each other by construction.
4. `tomland` provides abilities for `Generic` and `DerivingVia`
deriving out-of-the-box.
5. Despite being the fastest, `toml-parser` doesn’t support the array
of tables and because of that it’s hardly possible to specify the list
of custom data types in TOML with this library. In addition,
`toml-parser` doesn’t have ways to convert TOML AST to custom
Haskell types and `htoml*` libraries use typeclasses-based approach
via `aeson` library.

## Acknowledgement

Icons made by [Freepik](http://www.freepik.com) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/).