diff --git a/README.md b/README.md index aec761b3..9c328b57 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,30 @@ > “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 -Bidirectional TOML serialization. The following blog post has more details about -library design: +`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) +* [`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 new-run readme +cabal run readme ``` -> `tomland` supports TOML spec version 0.5.0. - ## Preamble: imports and language extensions Since this is a literate haskell file, we need to specify all our language @@ -41,15 +48,15 @@ extensions and imports up front. {-# LANGUAGE OverloadedStrings #-} import Control.Applicative ((<|>)) -import Control.Category ((>>>)) import Data.Text (Text) -import Toml (TomlBiMap, TomlCodec, (.=)) +import Data.Time (Day) +import Toml (TomlCodec, (.=)) import qualified Data.Text.IO as TIO import qualified Toml ``` -`tomland` is mostly designed for qualified imports and intended to be imported +`tomland` is designed for qualified imports and intended to be imported as follows: ```haskell ignore @@ -59,9 +66,39 @@ import qualified Toml ## Data type: parsing and printing -We're going to parse TOML configuration from [`examples/readme.toml`](examples/readme.toml) file. +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! +""" -This static configuration is captured by the following Haskell data type: +[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 @@ -78,25 +115,29 @@ data Mail = Mail } data User - = Admin !Integer -- id of admin - | Client !Text -- name of the client - deriving stock (Show) + = 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 `tomland` library, you can write bidirectional converters for these types -using the following guidelines and helper functions: +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 basic types like `Int` or `Text` you can just +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 this requires to specify - this data type as TOML table in `.toml` file. +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 primitive types + 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 @@ -123,23 +164,28 @@ mailCodec = Mail <$> Toml.diwrap (Toml.text "host") .= mailHost <*> Toml.bool "send-if-inactive" .= mailSendIfInactive -_Admin :: TomlBiMap User Integer -_Admin = Toml.prism Admin $ \case - Admin i -> Right i - other -> Toml.wrongConstructor "Admin" other +matchGuest :: User -> Maybe Integer +matchGuest = \case + Guest i -> Just i + _ -> Nothing -_Client :: TomlBiMap User Text -_Client = Toml.prism Client $ \case - Client n -> Right n - other -> Toml.wrongConstructor "Client" other +matchRegistered :: User -> Maybe RegisteredUser +matchRegistered = \case + Registered u -> Just u + _ -> Nothing userCodec :: TomlCodec User userCodec = - Toml.match (_Admin >>> Toml._Integer) "id" - <|> Toml.match (_Client >>> Toml._Text) "name" + 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're ready to parse our TOML and print the result back to see whether +And now we are ready to parse our TOML and print the result back to see whether everything is okay. ```haskell @@ -147,15 +193,19 @@ main :: IO () main = do tomlRes <- Toml.decodeFileEither settingsCodec "examples/readme.toml" case tomlRes of - Left err -> print err + Left errs -> TIO.putStrLn $ Toml.prettyTomlDecodeErrors errs Right settings -> TIO.putStrLn $ Toml.encode settingsCodec settings ``` ## Benchmarks and comparison with other libraries -`tomland` is [compared](https://github.com/kowainik/toml-benchmarks) with other libraries. Since it 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. +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 | |--------------------|----------------------|-----------------------------| @@ -164,21 +214,22 @@ this AST, benchmarks are also implemented in a way to reflect this difference. | `htoml-megaparsec` | `295.0 μs` | `33.62 μs` | | `toml-parser` | `164.6 μs` | `1.101 μs` | -You may see that `tomland` is not the fastest one (though still very fast). But -performance hasn’t been optimized so far and: - -1. `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. -2. `tomland` supports latest TOML spec while `htoml` and `htoml-megaparsec` - don’t have support for all types, values and formats. -3. `tomland` is the only library that has pretty-printing. -4. `toml-parser` doesn’t have ways to convert TOML AST to custom Haskell types - and `htoml*` libraries use typeclasses-based approach via `aeson` library. -5. `tomland` is bidirectional :slightly_smiling_face: +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/). +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/). diff --git a/examples/readme.toml b/examples/readme.toml index de8a225e..57911fbb 100644 --- a/examples/readme.toml +++ b/examples/readme.toml @@ -10,7 +10,11 @@ Don't touch it! send-if-inactive = false [[user]] - id = 42 + guestId = 42 [[user]] - name = "Foo Bar" + guestId = 114 + +[[user]] + login = "Foo Bar" + createdAt = 2020-05-19 diff --git a/tomland.cabal b/tomland.cabal index c90b0f4f..b69fbd66 100644 --- a/tomland.cabal +++ b/tomland.cabal @@ -134,8 +134,9 @@ executable readme if os(windows) buildable: False main-is: README.lhs - build-depends: text - , tomland + build-depends: tomland + , text + , time build-tool-depends: markdown-unlit:markdown-unlit ghc-options: -pgmL markdown-unlit