Skip to content
This repository has been archived by the owner on Oct 16, 2022. It is now read-only.

Commit

Permalink
Use RIO, fixes #11
Browse files Browse the repository at this point in the history
  • Loading branch information
valpackett committed Jul 1, 2018
1 parent 6ee8efa commit 7959fc8
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 90 deletions.
70 changes: 37 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
`An object appears at your feet! The voice of Anhur rings out: "Use my gift wisely!" a - an athame named Magicbane.`

# magicbane [![Hackage](https://img.shields.io/hackage/v/magicbane.svg?style=flat)](https://hackage.haskell.org/package/magicbane) [![Build Status](https://img.shields.io/travis/myfreeweb/magicbane.svg?style=flat)](https://travis-ci.org/myfreeweb/magicbane) [![unlicense](https://img.shields.io/badge/un-license-green.svg?style=flat)](http://unlicense.org)
[![Hackage](https://img.shields.io/hackage/v/magicbane.svg?style=flat)](https://hackage.haskell.org/package/magicbane)
[![Build Status](https://img.shields.io/travis/myfreeweb/magicbane.svg?style=flat)](https://travis-ci.org/myfreeweb/magicbane)
[![unlicense](https://img.shields.io/badge/un-license-green.svg?style=flat)](https://unlicense.org)

# magicbane

Magicbane is a Haskell framework for developing ops-friendly, high-performance, RESTful web services.

Okay, that's [Dropwizard](http://www.dropwizard.io)'s tagline. But just like Dropwizard in the Java world, Magicbane combines the best available Haskell libraries to provide a complete web development experience that reduces bikeshedding, wheel reinvention and the number of `import` lines. Hopefully :)
Okay, that's [Dropwizard](https://www.dropwizard.io)'s tagline.
But just like Dropwizard in the Java world, Magicbane combines the best available Haskell libraries to provide a complete web development experience that reduces bikeshedding, wheel reinvention and the number of `import` lines. Hopefully :)

In particular, Magicbane combines the following libraries:

- [Warp](https://www.stackage.org/package/warp) for HTTP.
- [RIO](https://github.com/commercialhaskell/rio) for the Prelude.
- [Servant](http://haskell-servant.readthedocs.io/en/stable/) for REST. It lets you describe web APIs with expressive type system features and implement request handlers with simple functions. Actually somewhat similar to JAX-RS/Jersey, but instead of annotations we have types, because it's Haskell instead of Java. The main feature of Magicbane is an easy way to add *stuff* (okay, let's call it "modules") on top of Servant.
- [Warp](https://www.stackage.org/package/warp) for HTTP.
- [Aeson](https://www.stackage.org/package/aeson) for JSON.
- [data-has](https://www.stackage.org/package/data-has) for extending the app context with services (modules). That thing remotely resembles dependency injection. But it's really cool!
- [envy](https://www.stackage.org/package/envy) for configuration. [Store config in environment variables](https://12factor.net/config)!
- [fast-logger](https://www.stackage.org/package/fast-logger)+[monad-logger](https://www.stackage.org/package/monad-logger) for logging. It works. It's fast. And it even lets you see what line of code produced a log message.
- [fast-logger](https://www.stackage.org/package/fast-logger) for logging. Integrated into RIO's logging API, and `monad-logger` as well for libraries that use it.
- [EKG](https://www.stackage.org/package/ekg)+[monad-metrics](https://www.stackage.org/package/monad-metrics) for metrics. `monad-metrics` lets you easily measure things in your application: just use `label`/`counter`/`distribution`/`gauge`/`timed` in your handlers. The EKG ecosystem has backends for [InfluxDB](https://www.stackage.org/package/ekg-influxdb), [Carbon (Graphite)](https://www.stackage.org/package/ekg-carbon), [statsd](https://www.stackage.org/package/ekg-statsd), [Prometheus](https://www.stackage.org/package/ekg-prometheus-adapter) and others… And a simple local [web server](https://www.stackage.org/package/ekg-wai) for development.
- [refined](https://nikita-volkov.github.io/refined/) for validation. Why use functions for input validation when you can use types? Magicbane integrates `refined` with Aeson, so you can write things like `count ∷ Refined Positive Int` in your data type definitions and inputs that don't satisfy the constraints will be rejected when input is processed.
- [http-client](https://www.stackage.org/package/http-client)([-tls](https://www.stackage.org/package/http-client-tls)) for, well, making HTTP requests. Most high level HTTP client libraries are built on top of that. Magicbane provides a small composable interface based on [http-conduit](https://www.stackage.org/package/http-conduit), which lets you e.g. stream the response body directly into [an HTML parser](https://www.stackage.org/package/html-conduit).
Expand All @@ -23,7 +29,6 @@ In particular, Magicbane combines the following libraries:

Not part of Magicbane, but recommended:

- [classy-prelude](https://www.stackage.org/package/classy-prelude) for the Prelude.
- [rapid](https://www.stackage.org/package/rapid) for fast development with GHCi hot reload.
- [hasql](https://www.stackage.org/package/hasql) for talking to PostgreSQL.
- [html-conduit](https://www.stackage.org/package/html-conduit) for parsing HTML.
Expand All @@ -38,19 +43,20 @@ Here's a hello world service. Just a simple file you can launch with [stack scri

```haskell
#!/usr/bin/env stack
{- stack runghc --package magicbane --package classy-prelude -}
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, DataKinds, TypeOperators, TemplateHaskell #-}
{- stack runghc --package magicbane -}
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, DataKinds, TypeOperators #-}
import RIO
import Magicbane
import ClassyPrelude

type HelloRoute = "hello" :> QueryParam "to" Text :> Get '[PlainText] Text
type ExampleAPI = HelloRoute
exampleAPI = Proxy Proxy ExampleAPI

hello Maybe Text BasicApp Text
hello x = do
$logInfo$ "Saying hello to " ++ tshow x
return $ "Hello " ++ (fromMaybe "anonymous" x) ++ "!"
let x' = fromMaybe "anonymous" x
logInfo $ "Saying hello to " <> display x'
return $ "Hello " <> x' <> "!"

main = do
ctx newBasicContext
Expand All @@ -64,46 +70,38 @@ That's just an example context for simple apps:

```haskell
type BasicContext = (ModHttpClient, ModLogger)
type BasicApp α = MagicbaneApp BasicContext α
```

Okay, what's `MagicbaneApp`?

```haskell
newtype MagicbaneApp β α = MagicbaneApp {
unMagicbaneApp ReaderT β IO α }
type BasicApp α = RIO BasicContext α
```

It's just a `ReaderT` over `IO`!

`ReaderT` combined with [data-has](https://www.stackage.org/package/data-has) makes it possible to have a beautifully extensible context.

And why isn't there `Handler` / `ExceptT` mentioned anywhere?
Well, [it's an antipattern](https://www.fpcomplete.com/blog/2016/11/exceptions-best-practices-haskell) that is now incompatible with [http-conduit](https://github.com/snoyberg/http-client/commit/dfbcb6c28a3216d0a69adfa9ccc8bdf62aff974d) (needs `MonadUnliftIO`) and [monad-metrics](https://github.com/parsonsmatt/monad-metrics/commit/17546b92b4e7e94279b81afe76fd6daa5f3ff0f8) (needs `MonadMask`). So Magicbane [got rid of Servant's ExceptT usage](https://www.parsonsmatt.org/2017/06/21/exceptional_servant_handling.html). To return a `servantErr`, just [`throwIO`](https://www.stackage.org/haddock/lts-10.9/unliftio-0.2.4.0/UnliftIO-Exception.html#v:throwIO) it.
Why isn't there `Handler` / `ExceptT` mentioned anywhere?
Well, [it's an antipattern](https://www.fpcomplete.com/blog/2016/11/exceptions-best-practices-haskell) that is now incompatible with [http-conduit](https://github.com/snoyberg/http-client/commit/dfbcb6c28a3216d0a69adfa9ccc8bdf62aff974d) (needs `MonadUnliftIO`) and [monad-metrics](https://github.com/parsonsmatt/monad-metrics/commit/17546b92b4e7e94279b81afe76fd6daa5f3ff0f8) (needs `MonadMask`).
So Magicbane [got rid of Servant's ExceptT usage](https://www.parsonsmatt.org/2017/06/21/exceptional_servant_handling.html).
To return a `servantErr`, just [`throwM`](https://www.stackage.org/haddock/lts-11.15/rio-0.1.3.0/RIO.html#v:throwM) ([`throwIO`](https://www.stackage.org/haddock/lts-10.9/unliftio-0.2.4.0/UnliftIO-Exception.html#v:throwIO)) it.

Anyway, let's make our own context instead of using the basic one:

```haskell
#!/usr/bin/env stack
{- stack runghc --package magicbane --package classy-prelude -}
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, DataKinds, TypeOperators, TemplateHaskell #-}
{- stack runghc --package magicbane -}
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, DataKinds, TypeOperators #-}
import RIO
import Magicbane
import ClassyPrelude

type MyAppContext = (ModLogger, ModMetrics)
type MyApp = MagicbaneApp MyAppContext
type MyApp = RIO MyAppContext

type HelloRoute = "hello" :> QueryParam "to" Text :> Get '[PlainText] Text
type ExampleAPI = HelloRoute
exampleAPI = Proxy Proxy ExampleAPI

hello Maybe Text MyApp Text
hello x = timed "hello" $ do
$logInfo$ "Saying hello to " ++ tshow x
return $ "Hello " ++ (fromMaybe "anonymous" x) ++ "!"
let x' = fromMaybe "anonymous" x
logInfo $ "Saying hello to " <> display x'
return $ "Hello " <> x' <> "!"

main = do
(_, modLogg) newLogger $ LogStderr defaultBufSize
(_, modLogg) newLogger (LogStderr defaultBufSize) simpleFormatter
metrStore serverMetricStore <$> forkMetricsServer "0.0.0.0" 8800
modMetr newMetricsWith metrStore
let ctx = (modLogg, modMetr)
Expand All @@ -123,17 +121,23 @@ Use [stack] to build.
$ stack build
```

And to run the examples:

```bash
$ stack exec runghc examples/larger.hs
```

[stack]: https://github.com/commercialhaskell/stack

## Contributing

Please feel free to submit pull requests!

By participating in this project you agree to follow the [Contributor Code of Conduct](http://contributor-covenant.org/version/1/4/).
By participating in this project you agree to follow the [Contributor Code of Conduct](https://contributor-covenant.org/version/1/4/).

## License

This is free and unencumbered software released into the public domain.
For more information, please refer to the `UNLICENSE` file or [unlicense.org](http://unlicense.org).
For more information, please refer to the `UNLICENSE` file or [unlicense.org](https://unlicense.org).

(However, the dependencies are not all unlicense'd!)
22 changes: 11 additions & 11 deletions examples/larger.hs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env stack
{- stack runghc --package magicbane --package classy-prelude -- +RTS -T -RTS -}
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, DeriveGeneric, DataKinds, TypeOperators, TemplateHaskell #-}
{- stack runghc --package magicbane -- +RTS -T -RTS -}
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, DeriveGeneric, DataKinds, TypeOperators #-}
import RIO
import Magicbane
import ClassyPrelude

data LargeAppConf = LargeAppConf
{ metricsPort Int
Expand All @@ -26,40 +26,40 @@ type ExampleAPI = HelloRoute :<|> SplinesRoute :<|> ReqRoute :<|> ErrRoute
exampleAPI = Proxy Proxy ExampleAPI

type LargeAppCtx = (ModLogger, ModMetrics, ModHttpClient, LargeAppConf)
type LargeApp = MagicbaneApp LargeAppCtx
type LargeApp = RIO LargeAppCtx

hello Maybe Text LargeApp Text
hello x = return $ "Hello " ++ (fromMaybe "anonymous" x) ++ "!"
hello x = return $ "Hello " <> (fromMaybe "anonymous" x) <> "!"

splines Maybe Int LargeApp Text
splines t = timed "app.important_work" $ do -- this easy to time an action
h askOpt metricsBind -- notice how the LargeAppConf type isn't mentioned anywhere except, well, the type of metricsBind? :)
p askOpt metricsPort -- yes, you can read properties (== call functions on) different parts of the context!
$logWarn $ "Reticulating splines... Check metrics at " ++ cs h ++ ":" ++ tshow p
logWarn $ "Reticulating splines... Check metrics at " <> displayBytesUtf8 h <> ":" <> display p
async $ do -- this easy to fork off a background job
let bgTime = (fromMaybe 1 t) * 2
threadDelay $ bgTime * 1000000
$logDebug $ "Also done some background work in " ++ tshow bgTime ++ " second(s)"
logDebug $ "Also done some background work in " <> display bgTime <> " second(s)"
let fgTime = (fromMaybe 1 t)
threadDelay $ fgTime * 1000000
gauge "app.important_work_last" fgTime
$logDebug $ "Done in " ++ tshow fgTime ++ " second(s)"
logDebug $ "Done in " <> display fgTime <> " second(s)"
return "done"

req LargeApp Text
req = timed "app.http_request" $ do
resp runHTTP $ performWithBytes =<< reqS (asText "https://httpbin.org/get")
resp runHTTP $ performWithBytes =<< reqS ("https://httpbin.org/get" :: Text)
return $ case resp of
Right resp' cs $ responseBody resp'
Left err err

showErr LargeApp Text
showErr = timed "app.err_throwing" $ do
throwIO $ errText err418 "Hello World"
throwM $ errText err418 "Hello World"

-- This isn't a Java framework, so there's no inversion of control with magical main :)
main = withEnvConfig $ \conf do
(_, modLogg) newLogger $ LogStderr defaultBufSize
(_, modLogg) newLogger (LogStderr defaultBufSize) simpleFormatter

-- In a serious app, you'd probably want ekg-influxdb/ekg-carbon/ekg-statsd/ekg-prometheus-adapter/…
-- But this web interface is pretty good in development
Expand Down
11 changes: 6 additions & 5 deletions examples/tiny.hs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
#!/usr/bin/env stack
{- stack runghc --package magicbane --package classy-prelude -}
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, DataKinds, TypeOperators, TemplateHaskell #-}
{- stack runghc --package magicbane -}
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, DataKinds, TypeOperators #-}
import RIO
import Magicbane
import ClassyPrelude

type HelloRoute = "hello" :> QueryParam "to" Text :> Get '[PlainText] Text
type ExampleAPI = HelloRoute
exampleAPI = Proxy Proxy ExampleAPI

hello Maybe Text BasicApp Text
hello x = do
$logInfo$ "Saying hello to " ++ tshow x
return $ "Hello " ++ (fromMaybe "anonymous" x) ++ "!"
let x' = fromMaybe "anonymous" x
logInfo $ "Saying hello to " <> display x'
return $ "Hello " <> x' <> "!"

main = do
ctx newBasicContext
Expand Down
6 changes: 3 additions & 3 deletions library/Magicbane.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module Magicbane (
, module Magicbane
) where

import Control.Error.Util as X hiding (hoistEither, (??), err, errLn, tryIO, handleExceptT, syncIO, bool)
import Control.Error.Util as X hiding ((??), err, errLn, tryIO, handleExceptT, syncIO, bool)
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe as X hiding (liftListen, liftPass, liftCallCC)
import UnliftIO.Exception as X hiding (Handler)
Expand Down Expand Up @@ -61,10 +61,10 @@ withEnvConfig a = decodeEnv >>= \case
hPutStrLn h s = liftIO $ System.IO.hPutStrLn h s

type BasicContext = (ModHttpClient, ModLogger)
type BasicApp α = MagicbaneApp BasicContext α
type BasicApp α = RIO BasicContext α

newBasicContext IO BasicContext
newBasicContext = do
http newHttpClient
(_, logg) newLogger $ LogStdout defaultBufSize
(_, logg) newLogger (LogStdout defaultBufSize) simpleFormatter
return (http, logg)
30 changes: 9 additions & 21 deletions library/Magicbane/App.hs
Original file line number Diff line number Diff line change
@@ -1,40 +1,28 @@
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings, UnicodeSyntax, DataKinds, TypeOperators, MultiParamTypeClasses, TypeFamilies, FlexibleContexts, FlexibleInstances, UndecidableInstances, GeneralizedNewtypeDeriving, CPP #-}
{-# LANGUAGE NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, UnicodeSyntax, DataKinds, TypeOperators, MultiParamTypeClasses, TypeFamilies, FlexibleContexts, FlexibleInstances, UndecidableInstances, GeneralizedNewtypeDeriving, CPP #-}

-- | Extends Servant with context.
-- Basically wrapping Servant in a ReaderT of your type.
-- Which should be a tuple of all your moudles and configs and stuff, so that the Data.Has module would let you access these items by type.
-- | Extends Servant with context, based on RIO.
-- The context should be a tuple of all your moudles and configs and stuff, so that the Data.Has module would let you access these items by type.
module Magicbane.App (
module X
, module Magicbane.App
, RIO
) where

import Control.Monad.Reader
import Control.Monad.Base (MonadBase)
import Control.Monad.Catch (MonadThrow, MonadCatch, MonadMask)
import Control.Monad.Trans.Control (MonadBaseControl)
import RIO
import RIO.Orphans as X ()
import Control.Monad.Trans.Except (ExceptT (..))
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.IO.Unlift (MonadUnliftIO)
import UnliftIO.Exception (try)
import Data.Proxy as X
import Data.Has as X
import Servant as X hiding (And, Handler)
import qualified Servant

newtype MagicbaneApp β α = MagicbaneApp {
unMagicbaneApp ReaderT β IO α
} deriving (Functor, Applicative, Monad,
MonadIO, MonadBase IO, MonadBaseControl IO, MonadUnliftIO,
MonadThrow, MonadCatch, MonadMask,
MonadReader β)

runMagicbaneHandler β MagicbaneApp β α Servant.Handler α
runMagicbaneHandler ctx a = Servant.Handler $ ExceptT $ try $ runReaderT (unMagicbaneApp a) ctx
runMagicbaneHandler β RIO β α Servant.Handler α
runMagicbaneHandler ctx a = Servant.Handler $ ExceptT $ try $ runReaderT (unRIO a) ctx

#if MIN_VERSION_servant_server(0,12,0)
#else
magicbaneToHandler β MagicbaneApp β :~> Servant.Handler
magicbaneToHandler β RIO β :~> Servant.Handler
magicbaneToHandler ctx = NT $ runMagicbaneHandler ctx
#endif

Expand Down
39 changes: 25 additions & 14 deletions library/Magicbane/Logging.hs
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE OverloadedStrings, UnicodeSyntax, FlexibleContexts, FlexibleInstances, UndecidableInstances #-}

-- | Provides logging via monad-logger/fast-logger in a Magicbane app context.
-- | Provides logging via fast-logger in a Magicbane app context.
module Magicbane.Logging (
module Magicbane.Logging
, module X
) where

import RIO
import Data.Has
import Data.Monoid
import Control.Monad.IO.Class
import Control.Monad.Logger as X
import Control.Monad.Reader
import System.Log.FastLogger
import System.Log.FastLogger as X (LogType(..), defaultBufSize)

newtype ModLogger = ModLogger (Loc LogSource LogLevel LogStr IO ())
type ModLogger = LogFunc

instance (Has ModLogger α, Monad μ, MonadIO μ, MonadReader α μ) MonadLogger μ where
monadLoggerLog loc src lvl msg = asks getter >>= \(ModLogger f) liftIO (f loc src lvl $ toLogStr msg)
instance Has ModLogger α HasLogFunc α where
logFuncL = hasLens

instance (Has ModLogger α, MonadIO μ, MonadReader α μ) MonadLoggerIO μ where
askLoggerIO = (\(ModLogger f) f) <$> asks getter
type Formatter = TimedFastLogger CallStack LogSource LogLevel Utf8Builder IO ()

-- | Creates a logger module. Also returns the logger itself for using outside of your Magicbane app (e.g. in some WAI middleware).
newLogger LogType IO (TimedFastLogger, ModLogger)
newLogger logtype = do
-- | Creates a logger module using a given formatting function.
-- | Also returns the underlying TimedFastLogger for use outside of your Magicbane app (e.g. in some WAI middleware).
newLogger LogType Formatter IO (TimedFastLogger, ModLogger)
newLogger logtype formatter = do
tc newTimeCache simpleTimeFormat'
(fl, _) newTimedFastLogger tc logtype
-- forget cleanup because the logger will exist for the lifetime of the (OS) process
return (fl, ModLogger $ \loc src lvl msg fl (\t toLogStr (t <> " ") <> defaultLogStr loc src lvl msg))
return (fl, mkLogFunc $ formatter fl)

simpleFormatter Formatter
simpleFormatter logger cs src level msg =
logger $ \t
toLogStr t <> " " <>
toLogStr (utf8BuilderToText $ displayCallStack cs) <> " " <>
toLogStr src <> " " <>
toLogStr (showLevel level) <>
toLogStr (utf8BuilderToText msg) <> "\n"
where showLevel LevelDebug = "[DEBUG] "
showLevel LevelInfo = "[ INFO] "
showLevel LevelWarn = "[ WARN] "
showLevel LevelError = "[ERROR] "
showLevel (LevelOther t) = "[" <> t <> "] "
Loading

0 comments on commit 7959fc8

Please sign in to comment.