servant-scotty (empty) → 0.1
raw patch · 8 files changed
+484/−0 lines, 8 filesdep +aesondep +basedep +http-typessetup-changed
Dependencies added: aeson, base, http-types, scotty, servant, servant-response, text, transformers
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- servant-scotty.cabal +33/−0
- src/Servant/Scotty.hs +87/−0
- src/Servant/Scotty/Arguments.hs +55/−0
- src/Servant/Scotty/Op.hs +110/−0
- src/Servant/Scotty/Prelude.hs +131/−0
- src/Servant/Scotty/Response.hs +36/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2014, Alp Mestanogullari++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Alp Mestanogullari nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ servant-scotty.cabal view
@@ -0,0 +1,33 @@+name: servant-scotty+version: 0.1+synopsis: Generate a web service for servant 'Resource's using scotty and JSON+description: Generate a web service for servant 'Resource's using scotty and JSON+homepage: http://github.com/zalora/servant+license: BSD3+license-file: LICENSE+author: Alp Mestanogullari+maintainer: alp@zalora.com+copyright: 2014 Zalora SEA+category: Web+build-type: Simple+cabal-version: >=1.10++library+ exposed-modules:+ Servant.Scotty+ , Servant.Scotty.Arguments+ , Servant.Scotty.Op+ , Servant.Scotty.Prelude+ , Servant.Scotty.Response+ build-depends:+ base >=4 && <5+ , http-types >= 0.8+ , servant >= 0.1+ , servant-response >= 0.1+ , scotty >= 0.8+ , text >= 0.11+ , transformers >= 0.4+ , aeson >= 0.7+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall
+ src/Servant/Scotty.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}++{- |+Module : Servant.Scotty+Copyright : (c) Zalora SEA 2014+License : BSD3+Maintainer : Alp Mestanogullari <alp@zalora.com>+Stability : experimental++Module for defining a <http://hackage.haskell.org/package/scotty scotty>+webservice from 'Resource's.++> EXAMPLE HERE+-}+module Servant.Scotty+ ( -- * Setting up handlers for a 'Resource'+ Runnable(runResource)+ -- * Defining handlers for an operation+ , ScottyOp(..)+ , Index(..)+ , js+ , safely+ , Response(..)+ , respond+ -- * Utilities+ , Suitables+ ) where++import Control.Applicative+import Control.Monad.IO.Class+import Data.Aeson hiding (json)+import GHC.Exts+import Servant.Resource+import Servant.Scotty.Arguments+import Servant.Scotty.Op+import Servant.Scotty.Response+import Web.Scotty.Trans++-- | Internal class used to drive the recursion+-- on the list of operations when generating all+-- the endpoints and handlers.+--+-- This lets everyone care only about the specific+-- behavior of their operations, this class will make+-- sure all the operations of your 'Resource' have an+-- implementation.+--+-- Regardless of what's+-- specific about each operation, we just recursively+-- go through all the operations and call 'runOperation'+-- for each of them.+class Runnable ops where+ -- | Call this function to setup a 'Resource' in your+ -- scotty application.+ runResource :: (Functor m, MonadIO m, ScottyError e, Suitables ops a i r)+ => Resource c a i r e ops+ -> ScottyT e m ()++-- | No operation means we don't setup any handler.+instance Runnable '[] where+ -- runResource :: (MonadIO m, ScottyError e)+ -- => Resource c a i r e '[]+ -- -> ScottyT e m ()+ -- no operation supported+ -- (or "no more", if there was any and we're ending the recursion)+ runResource _ = return ()++-- | Given some already runnable operation list @ops@,+-- and an operation that we can run in scotty+--+-- (that's the @'ScottyOp' o@ constraint),+--+-- we can run the @(o ': ops)@ operation list.+instance (ScottyOp o, Runnable ops) => Runnable (o ': ops) where+ -- runResource :: (MonadIO m, ScottyError e)+ -- => Resource c a i r e (o ': ops)+ -- -> ScottyT e m ()+ runResource r = do+ withHeadOperation r runOperation+ runResource (dropHeadOperation r)++type family Suitables (ops :: [*]) a i (r :: * -> *) :: Constraint+type instance Suitables '[] a i r = ()+type instance Suitables (o ': ops) a i r = (Suitable o a i r, Suitables ops a i r)
+ src/Servant/Scotty/Arguments.hs view
@@ -0,0 +1,55 @@+{- |+Module : Servant.Scotty.Arguments+Copyright : (c) Zalora SEA 2014+License : BSD3+Maintainer : Alp Mestanogullari <alp@zalora.com>+Stability : experimental++Functions and typeclasses for automatically fetching+the arguments of the \"database operations\"+from either the request path or from the request body,+by decoding it from JSON to your type.+-}+module Servant.Scotty.Arguments+ ( -- * Deducing an argument from the request path+ Index(..)+ , -- * Deducing an argument from the JSON body+ js+ , FromJSON+ ) where++import Control.Applicative+import Control.Monad.IO.Class+import Data.Aeson (FromJSON)+import Servant.Resource+import Web.Scotty.Trans++-- | What it means for a scotty 'Resource'+-- to have an index type.+--+-- * 'idx' should lookup in the request path+-- whatever is necessary to get the @i@+-- of @Resource c a i r e ops@, for operations+-- that take it as an argument, e.g /Delete/,+-- /Update/ or /View/.+--+-- * 'route' should return a 'String' that'll be+-- passed to 'capture'. You may use one or more+-- \"path parameters\" (calls to 'param', instances+-- of 'Param') to compute your value of type @k@.+-- You probably want to use 'name' on the 'Resource'+-- to generate the beginning of the path.+class Index k where+ -- | Lookup the index in the request path+ idx :: (Functor m, MonadIO m, ScottyError e)+ => ActionT e m k++ -- | String to 'capture' that represents the+ -- 'RoutePattern'.+ route :: Resource c a k r e ops -> String++-- | Simply gets the request's body as JSON+-- (or raises an exception if the decoding fails)+js :: (MonadIO m, ScottyError e, FromJSON a)+ => ActionT e m a+js = jsonData
+ src/Servant/Scotty/Op.hs view
@@ -0,0 +1,110 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{- |+Module : Servant.Scotty+Copyright : (c) Zalora SEA 2014+License : BSD3+Maintainer : Alp Mestanogullari <alp@zalora.com>+Stability : experimental++This module contains the class that you must implement+if you want some operation supported by the scotty backend and+some utilities you'll most likely need if you want to define your+own instances.++Import @Servant.Scotty.Prelude@ if you want instances for the+REST-y operations defined in @Servant.Prelude@.+-}+module Servant.Scotty.Op+ ( -- * The 'ScottyOp' class+ ScottyOp(..)+ , -- * Utility functions/classes you'll want to use+ -- for you own instances+ safely+ , Response(..)+ , respond+ , Index(..)+ , js+ ) where++import Control.Monad.IO.Class+import GHC.Exts+import Servant.Context+import Servant.Error+import Servant.Resource+import Servant.Scotty.Arguments+import Servant.Scotty.Response+import Web.Scotty.Trans++-- | A class that lets you define one or more handler(s) for an operation @o@.+class ScottyOp o where+ -- | Each operation can define its own constraints on:+ --+ -- * the type of the entries, @a@+ -- * the type by which the entries are indexed, @i@+ -- * the result type @r@ of \"effectful\" database operations+ -- (those that add/update/delete entries)+ --+ -- This is useful because that way, your types will /only/ have to+ -- satisfy the constraints /specified/ by the operations your 'Resource'+ -- carries, not some global dumb constraints you have to pay for even if+ -- you don't care about the operation that requires this constraint.+ type Suitable o a i (r :: * -> *) :: Constraint++ -- | Given a 'Resource' and the \"database function\" (so to speak)+ -- corresponding to your operation, do some business in /scotty/'s+ -- 'ScottyT' and 'ActionT' monads to define a handler for this very operation.+ --+ -- To provide the \"database function\" with some 'Context' @c@+ -- you can use 'Servant.Context.withContext' to run the operation+ -- and 'Servant.Resource.context' to get the context of your 'Resource'.+ --+ -- To catch exceptions around your db operation in your handler,+ -- you can use the 'Servant.Resource.excCatcher' access the+ -- 'Servant.Error.ExceptionCatcher' of your 'Resource' and+ -- 'Servant.Error.handledWith' to catch them and convert them+ -- to your error type @e@. You can then 'raise' the error value+ -- if you have a sensible default handler or handle it locally and+ -- respond with whatever is appropriate in your case.+ runOperation :: (Functor m, MonadIO m, ScottyError e, Suitable o a i r)+ => Resource c a i r e (o ': ops)+ -> Operation o c a i r+ -> ScottyT e m ()++-- | This is a function you'll want to use when defining your+-- own operations.+--+-- It runs the second argument to get the operation to run,+-- and feeds it the 'Resource' argument's context and extracts+-- the result.+--+-- Intended to be used in 'runOperation' in a way similar to+--+-- > -- example: for the 'Delete' operation+-- > runOperation res op =+-- > delete (capture $ "/" ++ name res ++ route res) $ do+-- > result <- safely res $ op <$> idx+-- > respond result+--+-- Here we just take the particular delete operation for the+-- client code's type, lookup the 'Index' argument it takes+-- from the request path and run the operation 'safely', eventually+-- converting its result to an appropriate response.+safely :: (MonadIO m, ScottyError e)+ => Resource c a i r e ops -- ^ the resource, that holds the context @c@+ -- and the exception catchers required to run+ -- the operation+ -> ActionT e m (c -> IO x) -- ^ a scotty action that'll produce the operation+ -- we want, with all the arguments but the context+ -- already provided.+ -> ActionT e m x -- ^ returns the result of the operation+ -- or 'raise' if an exception the 'Resource'+ -- is watching for is thrown, using the appropriate conversion+ -- to your application's error type @e@.+safely res mact = do+ act <- mact+ result <- liftIO $ withContext (context res) act+ `handledWith` excCatcher res+ either raise return result
+ src/Servant/Scotty/Prelude.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE DeriveGeneric,+ TypeFamilies,+ MultiParamTypeClasses,+ OverloadedStrings #-}+{- |+Module : Servant.Scotty.Prelude+Copyright : (c) Zalora SEA 2014+License : BSD3+Maintainer : Alp Mestanogullari <alp@zalora.com>+Stability : experimental++Instances of 'ScottyOp' for the operations defined+in "Servant.Prelude", along with some reusable types+necessary for the instances.+-}+module Servant.Scotty.Prelude+ ( -- * Defining 'Resource's and standard operations+ module Servant.Prelude++ , -- * Standard response types+ module Servant.Response.Prelude++ , -- * 'ScottyOp' class and standard operations implementations in scotty+ ScottyOp(..)++ , -- * Helpful types, functions, classes and instances+ -- for defining your own operations+ module Servant.Scotty.Arguments+ , module Servant.Scotty.Response+ , module Web.Scotty.Trans+ ) where++import Control.Applicative+import Data.Aeson+import Data.Text (Text)+import GHC.Generics+import Servant.Prelude+import Servant.Response.Prelude+import Servant.Scotty.Arguments+import Servant.Scotty.Op+import Servant.Scotty.Response+import Web.Scotty.Trans++-- | Generate a+--+-- > POST /:resourcename+--+-- handler for adding entries.+--+-- /Constraints on @a@, @i@ and @r@/:+--+-- > type Suitable Add a i r = (FromJSON a, Response (UpdateResponse Add) r)+instance ScottyOp Add where+ type Suitable Add a i r =+ (FromJSON a, Response (UpdateResponse Add) (r Add))++ runOperation res op =+ post (capture $ "/" ++ name res) $ do+ result <- safely res $ op <$> js+ respond result++-- | Generate a+--+-- > DELETE /:resourcename/<index specific stuffs>+--+-- handler for deleting entries.+--+-- /Constraints on @a@, @i@ and @r@/:+--+-- > type Suitable Delete a i r = (Index i, Response (UpdateResponse Delete) r)+instance ScottyOp Delete where+ type Suitable Delete a i r =+ (Index i, Response (UpdateResponse Delete) (r Delete))++ runOperation res op =+ delete (capture $ "/" ++ name res ++ route res) $ do+ result <- safely res $ op <$> idx+ respond result++-- | Generate a+--+-- > GET /:resourcename+--+-- handler for listing all entries.+--+-- /Constraints on @a@, @i@ and @r@/:+--+-- > type Suitable ListAll a i r = ToJSON a+instance ScottyOp ListAll where+ type Suitable ListAll a i r = ToJSON a++ runOperation res op =+ get (capture $ "/" ++ name res) $ do+ result <- safely res $ pure op+ respond result++-- | Generate a+--+-- > PUT /:resourcename/<index specific stuffs>+--+-- handler for updating an entry.+--+-- /Constraints on @a@, @i@ and @r@/:+--+-- > type Suitable Update a i r = (Index i, FromJSON a, Response (UpdateResponse Update) r)+instance ScottyOp Update where+ type Suitable Update a i r =+ (Index i, FromJSON a, Response (UpdateResponse Update) (r Update))++ runOperation res op =+ put (capture $ "/" ++ name res ++ route res) $ do+ result <- safely res $ op <$> idx <*> js+ respond result++-- | Generate a+--+-- > GET /:resourcename/<index specific stuffs>+--+-- handler for viewing an entry.+--+-- /Constraints on @a@, @i@ and @r@/:+--+-- > type Suitable View a i r = (Index i, ToJSON a)+instance ScottyOp View where+ type Suitable View a i r =+ (Index i, ToJSON a)++ runOperation res op =+ get (capture $ "/" ++ name res ++ route res) $ do+ result <- safely res $ op <$> idx+ respond result
+ src/Servant/Scotty/Response.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE FunctionalDependencies,+ MultiParamTypeClasses #-}+{- |+Module : Servant.Scotty.Response+Copyright : (c) Zalora SEA 2014+License : BSD3+Maintainer : Alp Mestanogullari <alp@zalora.com>+Stability : experimental++This module exports the 'Response' class and a handy 'respond'+function that you can use when defining handlers for your own+operations.++-}+module Servant.Scotty.Response+ ( -- * The 'Response' class+ module Servant.Response+ , -- * 'respond'+ respond+ ) where++import Servant.Response+import Web.Scotty.Trans++-- | Given the result of some operation,+-- it picks the appropriate response type+-- and uses 'toResponse' to convert the result+-- to a JSON-encodable value along with a status code,+-- both used to then send a response to the client.+respond :: (Response resp x, ScottyError e, Monad m)+ => x+ -> ActionT e m ()+respond result = do+ let (respValue, statuscode) = toResponse result+ status statuscode+ json respValue