Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/kowainik/tomland
- Owner: kowainik
- License: mpl-2.0
- Created: 2018-03-15T23:06:02.000Z (almost 7 years ago)
- Default Branch: main
- Last Pushed: 2024-06-07T13:14:33.000Z (7 months ago)
- Last Synced: 2024-10-02T18:47:52.948Z (3 months ago)
- Topics: bidirectional, configuration, hacktoberfest, haskell, profunctors, toml, toml-parser
- Language: Haskell
- Homepage: https://kowainik.github.io/posts/2019-01-14-tomland
- Size: 691 KB
- Stars: 120
- Watchers: 8
- Forks: 39
- Open Issues: 42
-
Metadata Files:
- Readme: README.lhs
- Changelog: CHANGELOG.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
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 userdata 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" .= settingsUsersmailCodec :: TomlCodec Mail
mailCodec = Mail
<$> Toml.diwrap (Toml.text "host") .= mailHost
<*> Toml.bool "send-if-inactive" .= mailSendIfInactivematchGuest :: User -> Maybe Integer
matchGuest = \case
Guest i -> Just i
_ -> NothingmatchRegistered :: User -> Maybe RegisteredUser
matchRegistered = \case
Registered u -> Just u
_ -> NothinguserCodec :: TomlCodec User
userCodec =
Toml.dimatch matchGuest Guest (Toml.integer "guestId")
<|> Toml.dimatch matchRegistered Registered registeredUserCodecregisteredUserCodec :: 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/).