Haskell plugin-based framework for any type of applications
Haskell
52
247 commits
updated Sep 24, 2023

Any PRs are welcome, even for documentation fixes. (The main author of this library is not an English native.)
Tonatona is a framework for any type of applications. It handles lots of boring tasks that are needed for real-world development such as reading in values defined in environment variables, setting up logging, sending emails, accessing databases, etc.
Tonatona can also be used with your favorite web framework as a glue. Tonatona does not provide the core functionalities
of web applications, such as routing, request parsing, response building, etc.
Instead, you can use plugins like tonatona-servant, tonatona-spock, or
tonatona-yesod to work with your favorite web framework.
Tonatona provides a plugin architecture so that anyone can add plugins implementing arbitrary functionality. This repository contains many standard plugins that are helpful when writing Haskell applications.
The most important goal of Tonatona is to make development speed fast and maintenance cost low.
In the Haskell community, you often hear things like, "Haskell makes it easy to maintain applications, but it takes a lot of time to create completely new, production-ready applications."
Tonatona's goal is to change this to "Haskell is great to maintain big applications, AND it is super-easy to create completely new, production-ready applications!"
Tonatona achieves this goal by providing a plugin-based architecture. There are many production-ready plugins to use in your own code. In order to start using a new plugin, often all you have to do is just import it! No need to specify configuration from within your application.
Using Tonatona is relatively simple. It requires declaring a few datatypes, as well as instances for classes provided by Tonatona.
This section describes how to do this, using our stack template for tonatona.
First, let's create a new tonatona project with stack new command:
$ stack new sample-app https://raw.githubusercontent.com/tonatona-project/tonatona/master/tonatona.hsfiles
This will create a new project named "sample-app".
Let’s start by just looking at all the code in sample-app/src/TonaApp/Main.hs.
module TonaApp.Main (app) where
import Tonalude
import Tonatona (HasConfig(..), HasParser(..))
import qualified Tonatona.Logger as TonaLogger
-- App
app :: RIO Config ()
app = do
-- Tonatona.Logger plugin enables to use logger functions without any configurations.
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("This is a debug message" :: Text)
-- Config
data Config = Config
{ tonaLogger :: TonaLogger.Config
-- , anotherPlugin :: TonaAnotherPlugin.Config
-- , yetAnotherPlugin :: TonaYetAnotherPlugin.Config
}
instance HasConfig Config TonaLogger.Config where
config = tonaLogger
instance HasParser Config where
parser = Config
<$> parser
-- <*> parser
-- <*> parser
As you can see import part, tonatona is supposed to be used with Tonalude as an alternative to Prelude.
import Tonalude
import Tonatona (HasConfig(..), HasParser(..))
import qualified Tonatona.Logger as TonaLogger
The main function named app has type of RIO Config (), instead of just IO ().
The Tonalude module and plugin modules (e.g., Tonatona.Logger) provides
bunch of convenient functions to be used in RIO Config monad.
app :: RIO Config ()
app = do
-- Tonatona.Logger plugin enables to use logger functions without any configurations.
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("This is a debug message" :: Text)
One of the amazing thing here is that there are no configurations about logging behaviour. The only thing you have to do is just write a little bit of boilerplate code.
data Config = Config
{ tonaLogger :: TonaLogger.Config
-- , anotherPlugin :: TonaAnotherPlugin.Config
-- , yetAnotherPlugin :: TonaYetAnotherPlugin.Config
}
instance HasConfig Config TonaLogger.Config where
config = tonaLogger
instance HasParser Config where
parser = Config
<$> parser
-- <*> parser
-- <*> parser
As comment implies, there are no dificulties to use other plugins. Just add boilerplate codes. It's all!
OK. It's time to compile it.
$ stack install --pedantic
So, let's see how it works.
$ stack exec sample-app
2018-11-18 21:15:09.594168: [info] This is a skeleton for tonatona project
@(src/TonaApp/Main.hs:16:3)
2018-11-18 21:15:09.594783: [debug] This is a debug message
@(src/TonaApp/Main.hs:17:3)
Wow, It actually works! But wait, it seems too verbose to run on production servers. Let's tell "sample-app" to act as production mode.
$ ENV=Production stack exec sample-app
This is a skeleton for tonatona project
Of course, all available environment variables and command line options can be displayed:
$ stack exec sample-app -- --help
Application deployment mode to run
Default: Development
Type: DeployMode
Command line option: --env
Environment variable: ENV
Make the operation more talkative
Default: False
Type: Bool
Command line option: --verbose
Environment variable: VERBOSE
...
...
This amazing feature is also provided by tonatona-logger plugin.
It is the power of plugin-based architecture tonatona provides.
First, we need to add new plugin to use in dependencies of package.yaml.
In this example, we use tonatona-persistent-sqlite plugin.
dependencies:
- base >= 4.14 && < 4.18
# `persistent` and `persistent-template` are also needed to
# actually use `tonatona-persistent-sqlite`.
- persistent
- persistent-template
- tonalude
- tonatona
- tonatona-logger
# new plugin to add
- tonatona-persistent-sqlite
Next, you need to add new field to Config.
import qualified Tonatona.Persist.Sqlite as TonaDb
data Config = Config
{ tonaLogger :: TonaLogger.Config
, tonaDb :: TonaDb.Config
}
Note that you have to import Tonatona.Persist.Sqlite module that tonatona-persistent-sqlite exposes.
Your Config data type will contain configuration values that can be read in on
the command line or through environment variables. For instance,
TonaDb.Config will contain the connection string for the SQLite database. By
default, this can be passed on the command line as --db-conn-string or as an
environment variable as DB_CONN_STRING. We will see this be used later.
Your Config data type should generally contain Tona*.Config data types, as
well as any of your own configuration options you would like to pick up from
the environment.
Tonatona needs to be told how to parse your Config data type from the
available command line flags and environment variables. The HasParser class is
used for this. The following is a simple example of this, for when your
Config just contains Tona*.Config data types:
instance HasParser Config where
parser = Config
<$> parser
<*> parser
Tonatona also requires a little bit of boilerplate code. You must help
Tonatona figure out how to get the TonaDb.Config from your Config. This is
done with the TonaDb.HasConfig class. This code should be very simple to
write:
instance HasConfig Config TonaDb.Config where
config = tonaDb
Now that we have all the easy code working, it is time to actually write your application!
First, we need to create a table definition. The following creates a table to
hold blog posts. It will have 3 columns: id, author_name, and contents.
Creating a table definition is a requirement for using
persistent, which is what
tonatona-persistent-sqlite is using internally. This is not a requirement for
Tonatona in general, just the tonatona-persistent-sqlite package.
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
import Database.Persist.TH (mkMigrate, mkPersist, persistLowerCase, share, sqlSettings)
share
[mkPersist sqlSettings, mkMigrate "migrateAll"]
[persistLowerCase|
BlogPost
authorName Text
contents Text
deriving Show
|]
Next, do some DB operations in app function:
import Database.Persist (insert_)
app :: RIO Config ()
app = do
-- Tonatona.Logger plugin enables to use logger functions without any configurations.
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("Migrating DB..." :: Text)
TonaDb.runMigrate migrateAll
TonaLogger.logDebug $ display ("Running DB query..." :: Text)
TonaDb.run $ do
-- By using 'lift', any plugins are available in @TonaDb.run@.
lift $
TonaLogger.logInfo $ display $
("This log is called inside of `TonaDb.run`" :: Text)
insert_ $ BlogPost "Mr. Foo Bar" "This is an example blog post"
TonaLogger.logInfo $ display ("Successfully inserted a blog post!" :: Text)
A summary of the steps you need to take is as follows:
Create a Config data type for your application. If you want
to use multiple plugins, just have your data types hold multiple Tona*.Config.
Create a HasParser instance for your Config data type.
Create a Tona*.HasConfig instance for each of the plugins you are using.
Actually write your application using the RIO Config () monad.
Tonatona has many plugins available. Here are the plugins provided in this repository.
tonatona-persistent-postgresql
Provide access to a PostgreSQL database through the persistent library.
Provide access to a SQLite database through the persistent library.
Provide a way to log to the console at runtime using monad-logger.
Provide an easy way to run a servant server.
Tonatona has the additional general features that apply to every plugin:
Make the end-user write a little boilerplate code up front in order to provide ease-of-use when writing their own business logic.
End users should be able to use many plugins without any configuration code or setup code. Plugins should be configured to get configuration options from environment variables, command line flags, etc. Plugins should be configured with reasonable defaults.
Information about contributing new plugins can be found in CONTRIBUTING.md.
In general, new plugins will be accepted to Tonatona if they are widely useful. For instance, a plugin adding support for a widely used database library will probably be accepted, while a plugin adding support for a proprietary library not widely used will probably not be accepted.
If your plugin is not accepted into this repository, you are free to support it as a third-party repository, release it on Hackage, etc. If you are using Tonatona in a larger project, you will probably end up creating a few of your own plugins!
Haskell
81.3%
Makefile
18.7%
Haskell plugin-based framework for any type of applications
Haskell
52
247 commits
updated Sep 24, 2023

Any PRs are welcome, even for documentation fixes. (The main author of this library is not an English native.)
Tonatona is a framework for any type of applications. It handles lots of boring tasks that are needed for real-world development such as reading in values defined in environment variables, setting up logging, sending emails, accessing databases, etc.
Tonatona can also be used with your favorite web framework as a glue. Tonatona does not provide the core functionalities
of web applications, such as routing, request parsing, response building, etc.
Instead, you can use plugins like tonatona-servant, tonatona-spock, or
tonatona-yesod to work with your favorite web framework.
Tonatona provides a plugin architecture so that anyone can add plugins implementing arbitrary functionality. This repository contains many standard plugins that are helpful when writing Haskell applications.
The most important goal of Tonatona is to make development speed fast and maintenance cost low.
In the Haskell community, you often hear things like, "Haskell makes it easy to maintain applications, but it takes a lot of time to create completely new, production-ready applications."
Tonatona's goal is to change this to "Haskell is great to maintain big applications, AND it is super-easy to create completely new, production-ready applications!"
Tonatona achieves this goal by providing a plugin-based architecture. There are many production-ready plugins to use in your own code. In order to start using a new plugin, often all you have to do is just import it! No need to specify configuration from within your application.
Using Tonatona is relatively simple. It requires declaring a few datatypes, as well as instances for classes provided by Tonatona.
This section describes how to do this, using our stack template for tonatona.
First, let's create a new tonatona project with stack new command:
$ stack new sample-app https://raw.githubusercontent.com/tonatona-project/tonatona/master/tonatona.hsfiles
This will create a new project named "sample-app".
Let’s start by just looking at all the code in sample-app/src/TonaApp/Main.hs.
module TonaApp.Main (app) where
import Tonalude
import Tonatona (HasConfig(..), HasParser(..))
import qualified Tonatona.Logger as TonaLogger
-- App
app :: RIO Config ()
app = do
-- Tonatona.Logger plugin enables to use logger functions without any configurations.
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("This is a debug message" :: Text)
-- Config
data Config = Config
{ tonaLogger :: TonaLogger.Config
-- , anotherPlugin :: TonaAnotherPlugin.Config
-- , yetAnotherPlugin :: TonaYetAnotherPlugin.Config
}
instance HasConfig Config TonaLogger.Config where
config = tonaLogger
instance HasParser Config where
parser = Config
<$> parser
-- <*> parser
-- <*> parser
As you can see import part, tonatona is supposed to be used with Tonalude as an alternative to Prelude.
import Tonalude
import Tonatona (HasConfig(..), HasParser(..))
import qualified Tonatona.Logger as TonaLogger
The main function named app has type of RIO Config (), instead of just IO ().
The Tonalude module and plugin modules (e.g., Tonatona.Logger) provides
bunch of convenient functions to be used in RIO Config monad.
app :: RIO Config ()
app = do
-- Tonatona.Logger plugin enables to use logger functions without any configurations.
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("This is a debug message" :: Text)
One of the amazing thing here is that there are no configurations about logging behaviour. The only thing you have to do is just write a little bit of boilerplate code.
data Config = Config
{ tonaLogger :: TonaLogger.Config
-- , anotherPlugin :: TonaAnotherPlugin.Config
-- , yetAnotherPlugin :: TonaYetAnotherPlugin.Config
}
instance HasConfig Config TonaLogger.Config where
config = tonaLogger
instance HasParser Config where
parser = Config
<$> parser
-- <*> parser
-- <*> parser
As comment implies, there are no dificulties to use other plugins. Just add boilerplate codes. It's all!
OK. It's time to compile it.
$ stack install --pedantic
So, let's see how it works.
$ stack exec sample-app
2018-11-18 21:15:09.594168: [info] This is a skeleton for tonatona project
@(src/TonaApp/Main.hs:16:3)
2018-11-18 21:15:09.594783: [debug] This is a debug message
@(src/TonaApp/Main.hs:17:3)
Wow, It actually works! But wait, it seems too verbose to run on production servers. Let's tell "sample-app" to act as production mode.
$ ENV=Production stack exec sample-app
This is a skeleton for tonatona project
Of course, all available environment variables and command line options can be displayed:
$ stack exec sample-app -- --help
Application deployment mode to run
Default: Development
Type: DeployMode
Command line option: --env
Environment variable: ENV
Make the operation more talkative
Default: False
Type: Bool
Command line option: --verbose
Environment variable: VERBOSE
...
...
This amazing feature is also provided by tonatona-logger plugin.
It is the power of plugin-based architecture tonatona provides.
First, we need to add new plugin to use in dependencies of package.yaml.
In this example, we use tonatona-persistent-sqlite plugin.
dependencies:
- base >= 4.14 && < 4.18
# `persistent` and `persistent-template` are also needed to
# actually use `tonatona-persistent-sqlite`.
- persistent
- persistent-template
- tonalude
- tonatona
- tonatona-logger
# new plugin to add
- tonatona-persistent-sqlite
Next, you need to add new field to Config.
import qualified Tonatona.Persist.Sqlite as TonaDb
data Config = Config
{ tonaLogger :: TonaLogger.Config
, tonaDb :: TonaDb.Config
}
Note that you have to import Tonatona.Persist.Sqlite module that tonatona-persistent-sqlite exposes.
Your Config data type will contain configuration values that can be read in on
the command line or through environment variables. For instance,
TonaDb.Config will contain the connection string for the SQLite database. By
default, this can be passed on the command line as --db-conn-string or as an
environment variable as DB_CONN_STRING. We will see this be used later.
Your Config data type should generally contain Tona*.Config data types, as
well as any of your own configuration options you would like to pick up from
the environment.
Tonatona needs to be told how to parse your Config data type from the
available command line flags and environment variables. The HasParser class is
used for this. The following is a simple example of this, for when your
Config just contains Tona*.Config data types:
instance HasParser Config where
parser = Config
<$> parser
<*> parser
Tonatona also requires a little bit of boilerplate code. You must help
Tonatona figure out how to get the TonaDb.Config from your Config. This is
done with the TonaDb.HasConfig class. This code should be very simple to
write:
instance HasConfig Config TonaDb.Config where
config = tonaDb
Now that we have all the easy code working, it is time to actually write your application!
First, we need to create a table definition. The following creates a table to
hold blog posts. It will have 3 columns: id, author_name, and contents.
Creating a table definition is a requirement for using
persistent, which is what
tonatona-persistent-sqlite is using internally. This is not a requirement for
Tonatona in general, just the tonatona-persistent-sqlite package.
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
import Database.Persist.TH (mkMigrate, mkPersist, persistLowerCase, share, sqlSettings)
share
[mkPersist sqlSettings, mkMigrate "migrateAll"]
[persistLowerCase|
BlogPost
authorName Text
contents Text
deriving Show
|]
Next, do some DB operations in app function:
import Database.Persist (insert_)
app :: RIO Config ()
app = do
-- Tonatona.Logger plugin enables to use logger functions without any configurations.
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("Migrating DB..." :: Text)
TonaDb.runMigrate migrateAll
TonaLogger.logDebug $ display ("Running DB query..." :: Text)
TonaDb.run $ do
-- By using 'lift', any plugins are available in @TonaDb.run@.
lift $
TonaLogger.logInfo $ display $
("This log is called inside of `TonaDb.run`" :: Text)
insert_ $ BlogPost "Mr. Foo Bar" "This is an example blog post"
TonaLogger.logInfo $ display ("Successfully inserted a blog post!" :: Text)
A summary of the steps you need to take is as follows:
Create a Config data type for your application. If you want
to use multiple plugins, just have your data types hold multiple Tona*.Config.
Create a HasParser instance for your Config data type.
Create a Tona*.HasConfig instance for each of the plugins you are using.
Actually write your application using the RIO Config () monad.
Tonatona has many plugins available. Here are the plugins provided in this repository.
tonatona-persistent-postgresql
Provide access to a PostgreSQL database through the persistent library.
Provide access to a SQLite database through the persistent library.
Provide a way to log to the console at runtime using monad-logger.
Provide an easy way to run a servant server.
Tonatona has the additional general features that apply to every plugin:
Make the end-user write a little boilerplate code up front in order to provide ease-of-use when writing their own business logic.
End users should be able to use many plugins without any configuration code or setup code. Plugins should be configured to get configuration options from environment variables, command line flags, etc. Plugins should be configured with reasonable defaults.
Information about contributing new plugins can be found in CONTRIBUTING.md.
In general, new plugins will be accepted to Tonatona if they are widely useful. For instance, a plugin adding support for a widely used database library will probably be accepted, while a plugin adding support for a proprietary library not widely used will probably not be accepted.
If your plugin is not accepted into this repository, you are free to support it as a third-party repository, release it on Hackage, etc. If you are using Tonatona in a larger project, you will probably end up creating a few of your own plugins!
Haskell
81.3%
Makefile
18.7%