gingersnap 0.1.3.0 → 0.1.4.0
raw patch · 3 files changed
+185/−5 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Gingersnap.Core: createPool :: () => IO a -> a -> IO () -> Int -> NominalDiffTime -> Int -> IO Pool a
+ Gingersnap.Core: data Connection
+ Gingersnap.Core: data Pool a
Files
- README.md +170/−0
- gingersnap.cabal +7/−3
- src/Gingersnap/Core.hs +8/−2
+ README.md view
@@ -0,0 +1,170 @@+# Gingersnap++What _is_ Gingersnap? Not a web framework: that's Snap Core's job.+More a set of lightweight idioms for building a resource-safe JSON API with+Aeson and postgresql-simple.++As it's just a set of idioms, it's easy to only use 'em where you need 'em.+An app could have only a single endpoint that uses Gingersnap, with the rest+using plain Snap Core.++How do we use it? This README is also a Literate Haskell file so it's a full+example you can run with markdown-unlit. Let's get started:++## Imports at the top!++A few imports we'll need for this tutorial:++```haskell+{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}++import Data.Aeson (ToJSON)+import qualified Database.PostgreSQL.Simple as PSQL+ -- For our automatic JSON instance:+import GHC.Generics (Generic)+import Gingersnap.Core+import Snap.Core+-- From the 'snap-server' package:+import Snap.Http.Server (quickHttpServe)+```++## A first endpoint++Now that we've got our imports, let's jump into defining an endpoint. We'll+define a little bit of setup code later on in the file.++```haskell+data SomeData = SomeData Int Bool+ deriving (Show, Generic)++instance ToJSON SomeData++one :: Ctx -> Snap ()+one ctx =+ pureRsp ctx $ rspGood $ SomeData 5 True+```++You can run the code from this file with++ $ cabal update+ $ cabal install gingersnap snap-server+ $ ghci -pgmL markdown-unlit README.lhs # The "-pgmL" is just for this README++And calling "main". In another window, if you call:++ $ curl 'localhost:8000/one'++You should get back:++ {"result":[5,true]}++A few things to notice:+ - The endpoint takes as an argument a "Ctx". We'll see the definition of that+ later.+ - The endpoint returns our data with "rspGood". More on that in a moment.+ - The response came wrapped in a "result" JSON object. You can customize that+ behavior but we'll use the default here.++So, what's "rspGood"? Well, it has the type++ rspGood :: ToJSON x => x -> Rsp++The "Rsp" type is one of the core types in Gingersnap. Keep an eye out for it later.++## Defining "main"++Let's now look at how we defined our main function.++```haskell+main :: IO ()+main = do+ ctx <- makeCtx+ quickHttpServe $ route [+ ("one", one ctx)+ , ("two", two ctx)+ ]+```++Other than "ctx", this isn't Gingersnap-specific at all: just a simple+Snap Core server. "makeCtx" is a function we define ourselves. It creates a+value of type "Ctx", which we define ourselves, and which is an instance of+"IsCtx".++## IsCtx++The idea of "IsCtx" is that it allows us to thread whatever data we need through+to our endpoints. We'll definitely need a database connection (pool), but it's a+typeclass, so you can define whatever other fields you'd like to pass to your+handlers in the type that's an instance of that class.++For example, if you're using the 'auto-update' package to efficiently run+periodic actions (like getting the current time), you may want to create another+set of fields in your "Ctx" type to easily thread auto-update's actions through+to your handlers, too.++So let's define our own! We unimaginitavely call it "Ctx":++```haskell+data Ctx = Ctx { ctx_db :: Pool PSQL.Connection }++instance IsCtx Ctx where+ ctxConnectionPool = ctx_db+```++And then define a simple "makeCtx":++```haskell+makeCtx :: IO Ctx+makeCtx = do++ -- Setting up the DB connection pool:+ let connString = " host=localhost port=5432 dbname=postgres user=postgres "+ pool <- createPool (PSQL.connectPostgreSQL connString) PSQL.close 1 5 20++ pure $ Ctx { ctx_db = pool }+```++And that's that!++## Talking to the database++Now that we've got (through Ctx) a DB connection pool, let's query the DB:+++```haskell+two :: Ctx -> Snap ()+two ctx = do+ inTransaction ctx $ \conn -> do+ [PSQL.Only x] <- PSQL.query_ conn " SELECT 2 + 2 "+ pure $ rspGood $ SomeData x True+```++ $ curl 'localhost:8000/two'+ {"result":[4,true]}++Nice! This uses "inTransaction", another core tool in Gingersnap:++ inTransaction :: IsCtx ctx => ctx -> (Connection -> IO Rsp) -> Snap ()++We've already seen IsCtx and Rsp, so the main thing to notice here is that the+action we pass to inTransaction is passed a Connection and is in IO, not in Snap+(or MonadSnap). This gives us a few nice things:+ - If you're in Snap, you can accidentally leak a Connection resource by calling+ finishWith while in the middle of a transaction or DB action. We don't have this+ problem.+ - We can choose at any time whether to commit or rollback. The "Rsp" type carries+ information about whether to commit or rollback (e.g. "rspGood" will commit,+ "rspBadRollback" won't). This is in contrast to "withTransaction", that'll+ only roll back if there's an exception, and in contrast to manually beginning+ a transaction, which provides no check that we properly commit or rollback+ (e.g. we could forget to commit/rollback in one case among many in a complicated+ statement)+++## Other things to discover++This tutorial is a work in progress, and these'll be the next concepts to be+touched on:++ - "Not everything's rspGood": rspBadRollback, ApiErr, and others+ - reqObject and (.!) for consuming JSON
gingersnap.cabal view
@@ -1,7 +1,9 @@ name: gingersnap-version: 0.1.3.0+version: 0.1.4.0 synopsis: snap-core + aeson + postgresql-simple = delicious-description: JSON API idioms for snap-core, preventing DB connection leaks+description:+ JSON API idioms for snap-core, preventing DB connection leaks.+ See the README for a tutorial and example use. license: BSD3 license-file: LICENSE author: Tinybop Labs, tom-bop@@ -10,7 +12,9 @@ -- copyright: category: Web build-type: Simple-extra-source-files: ChangeLog.md+extra-source-files:+ ChangeLog.md+ README.md cabal-version: >=1.10 source-repository head
src/Gingersnap/Core.hs view
@@ -36,6 +36,12 @@ , RspPayload(..) , ShouldCommitOrRollback(..) ++ -- * Reexports, for convenience+ , Pool+ , createPool+ , Connection+ -- Maybe?: -- , module import Network.HTTP.Types.Status -- , module Snap.Core@@ -49,7 +55,7 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as BSL-import Data.Pool (Pool)+import Data.Pool (Pool, createPool) import qualified Data.Pool as Pool import Database.PostgreSQL.Simple (Connection) import qualified Database.PostgreSQL.Simple as PSQL@@ -110,7 +116,7 @@ data RspPayload = forall x. ToJSON x => RspPayload_Good x | forall e. ApiErr e => RspPayload_Bad e- -- First ByteString is MIME type; second is response body:+ -- | First ByteString is MIME type; second is response body | RspPayload_Custom HTTP.Status BS.ByteString BSL.ByteString | RspPayload_Empty -- This might be a dupe with '_Custom' but it's nice to have