moonshine 1.2.2.1 → 2.0.0.0
raw patch · 2 files changed
+57/−419 lines, 2 filesdep −directorydep −filepathdep ~canteven-configPVP ok
version bump matches the API change (PVP)
Dependencies removed: directory, filepath
Dependency ranges changed: canteven-config
API changes (from Hackage documentation)
- Web.Moonshine: BadEntity :: String -> DecodeResult e
- Web.Moonshine: LoggingConfig :: LogPriority -> Maybe FilePath -> [LoggerDetails] -> LoggingConfig
- Web.Moonshine: Ok :: e -> DecodeResult e
- Web.Moonshine: Unsupported :: DecodeResult e
- Web.Moonshine: assertMethod :: Method -> Snap a -> Snap a
- Web.Moonshine: badRequest :: String -> Snap a
- Web.Moonshine: class RequestEntity e
- Web.Moonshine: class ResponseEntity e
- Web.Moonshine: conflict :: Snap ()
- Web.Moonshine: created :: Snap ()
- Web.Moonshine: data DecodeResult e
- Web.Moonshine: data LoggingConfig
- Web.Moonshine: decodeEntity :: RequestEntity e => Maybe ContentType -> ByteString -> DecodeResult e
- Web.Moonshine: exactPath :: Snap a -> Snap a
- Web.Moonshine: getBytes :: ResponseEntity e => e -> ByteString
- Web.Moonshine: getContentType :: ResponseEntity e => e -> ContentType
- Web.Moonshine: getMethod :: Snap Method
- Web.Moonshine: getUserConfig :: (MonadIO io, FromJSON config) => io config
- Web.Moonshine: instance FromJSON LogPriority
- Web.Moonshine: instance FromJSON LoggerDetails
- Web.Moonshine: instance FromJSON LoggingConfig
- Web.Moonshine: instance FromJSON SystemConfig
- Web.Moonshine: level :: LoggingConfig -> LogPriority
- Web.Moonshine: logfile :: LoggingConfig -> Maybe FilePath
- Web.Moonshine: loggers :: LoggingConfig -> [LoggerDetails]
- Web.Moonshine: methodNotAllowed :: [Method] -> Snap a
- Web.Moonshine: noContent :: Snap ()
- Web.Moonshine: notFound :: Snap a
- Web.Moonshine: readEntity :: RequestEntity e => Int64 -> Snap e
- Web.Moonshine: readJSON :: FromJSON json => Int64 -> Snap json
- Web.Moonshine: requiredParam :: ByteString -> Snap ByteString
- Web.Moonshine: requiredUtf8Param :: ByteString -> Snap Text
- Web.Moonshine: setServerVersion :: String -> Version -> Moonshine ()
- Web.Moonshine: type ContentType = ByteString
- Web.Moonshine: unsupportedMediaType :: Snap a
- Web.Moonshine: writeEntity :: ResponseEntity e => e -> Snap ()
- Web.Moonshine: writeJSON :: ToJSON json => json -> Snap ()
Files
- moonshine.cabal +3/−5
- src/Web/Moonshine.hs +54/−414
moonshine.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: moonshine-version: 1.2.2.1+version: 2.0.0.0 synopsis: A web service framework for Haskell, similar in purpose to dropwizard. -- description: homepage: https://github.com/SumAll/moonshine@@ -22,14 +22,12 @@ -- other-modules: other-extensions: DeriveGeneric build-depends:- base >= 4.7 && < 5.0,- canteven-config >= 0.1 && < 0.2, aeson,+ base >= 4.7 && < 5.0, bytestring,- directory,+ canteven-config >= 1.0 && < 1.1, ekg >= 0.4.0.4, ekg-core,- filepath, hslogger, snap, text,
src/Web/Moonshine.hs view
@@ -1,48 +1,50 @@ {-# LANGUAGE DeriveGeneric, OverloadedStrings, NamedFieldPuns #-} module Web.Moonshine (- Moonshine, LoggingConfig(..), runMoonshine, route, makeTimer,- timerAdd, Timer, getUserConfig, timed, liftSnap, liftIO, badRequest,- requiredParam, requiredUtf8Param, writeJSON, setBanner, readJSON,- created, noContent, methodNotAllowed, assertMethod, timedIO,- makeGauge, gaugeInc, gaugeDec, gaugeSet, gaugeAdd, gaugeSubtract,- Gauge, notFound, setServerVersion, ResponseEntity(..), writeEntity,- getMethod, conflict, ContentType, RequestEntity(..), DecodeResult(..),- readEntity, unsupportedMediaType, exactPath+ Moonshine,+ runMoonshine,+ route,+ makeTimer,+ timerAdd,+ Timer,+ timed,+ liftSnap,+ liftIO,+ setBanner,+ timedIO,+ makeGauge,+ gaugeInc,+ gaugeDec,+ gaugeSet,+ gaugeAdd,+ gaugeSubtract,+ Gauge, ) where import Canteven.Config (canteven) import Control.Applicative (Applicative(pure, (<*>)), (<|>)) import Control.Monad (ap, liftM) import Control.Monad.IO.Class (MonadIO(liftIO))-import Data.Aeson (Value(String, Object), (.:?), (.!=), ToJSON, encode,- eitherDecode, (.:), (.!=))+import Data.Aeson (Value(Object), (.:?), (.!=), (.!=)) import Data.ByteString (ByteString)-import Data.ByteString.Lazy (toStrict) import Data.Int (Int64)-import Data.List (intercalate) import Data.Maybe (fromMaybe) import Data.Monoid (mempty, mconcat) import Data.Text (Text)-import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Data.Text.Encoding (decodeUtf8) import Data.Time.Clock (getCurrentTime, diffUTCTime)-import Data.Version (Version, showVersion) import Data.Yaml (FromJSON(parseJSON)) import GHC.Generics (Generic) import Snap (Snap, httpServe, setPort, Config, setSSLCert, setSSLKey,- setSSLPort, getParam, writeBS, setResponseCode, getResponse,- finishWith, modifyResponse, setHeader, setAccessLog, setErrorLog,- ConfigLog(ConfigIoLog), readRequestBody, Method(Method), getsRequest,- rqMethod, getHeader, rqPathInfo, pass)+ setSSLPort, setResponseCode, getResponse, finishWith, setAccessLog,+ setErrorLog, ConfigLog(ConfigIoLog)) import System.Environment (setEnv, lookupEnv)-import System.Log (Priority(INFO)) import System.Log.Logger (infoM, errorM) import System.Metrics (registerGcMetrics) import System.Metrics.Distribution (Distribution) import System.Remote.Monitoring (Server, forkServerWith)-import qualified Data.ByteString.Lazy as L (ByteString)-import qualified Data.Text as T+import qualified Data.Text as T (unpack) import qualified Snap (route)-import qualified System.Log.Logger as L (debugM, warningM)+import qualified System.Log.Logger as L (debugM) import qualified System.Metrics as EkgMetrics import qualified System.Metrics.Distribution as D (add) import qualified System.Metrics.Gauge as G (inc, Gauge, dec, set, add, subtract)@@ -88,61 +90,13 @@ newtype Gauge = G {unGauge :: G.Gauge} -{- |- Logging configuration that Moonshine should use to initialize logging.-- This type is an instance of FromJSON, so you can easily use it in your- configuration as:-- @- data MyConfig = MyConfig {- logging :: LoggingConfig- } deriving (Generic)- instance FromJSON MyConfig- @--}-data LoggingConfig =- LoggingConfig {- level :: LogPriority,- logfile :: Maybe FilePath,- loggers :: [LoggerDetails]- }--instance FromJSON LoggingConfig where- parseJSON (Object logging) = do- level <- logging .:? "level" .!= LP INFO- logfile <- logging .:? "logfile"- loggers <- logging .:? "loggers" .!= []- return LoggingConfig {level, logfile, loggers}- parseJSON value =- fail $ "Couldn't parse logging config from value " ++ show value---{- |- A way to set more fined-grained configuration for specific loggers.--}-data LoggerDetails =- LoggerDetails {- loggerName :: String,- loggerLevel :: LogPriority- }--instance FromJSON LoggerDetails where- parseJSON (Object details) = do- loggerName <- details .: "logger"- loggerLevel <- details .: "level"- return LoggerDetails {loggerName, loggerLevel}- parseJSON value =- fail $ "Couldn't parse logger details from value " ++ show value-- data ServerConfig = ServerConfig {- applicationConnector :: [ConnectorConfig]- -- | Configuration for EKG. Note that although this is a list,- -- at present EKG makes it impossible to support more than one- -- server. See https://github.com/tibbe/ekg/issues/32.- , adminConnector :: [ConnectorConfig]+ applicationConnector :: [ConnectorConfig],+ adminConnector :: [ConnectorConfig]+ -- ^ Configuration for EKG. Note that although this is a list,+ -- at present EKG makes it impossible to support more than one+ -- server. See https://github.com/tibbe/ekg/issues/32. } deriving (Generic) instance FromJSON ServerConfig@@ -164,18 +118,6 @@ instance FromJSON Scheme -{- |- A wrapper for Priority, so we can avoid orphan instances--}-newtype LogPriority = LP Priority--instance FromJSON LogPriority where- parseJSON (String s) = case reads (T.unpack s) of- [(priority, "")] -> return (LP priority)- _ -> fail $ "couldn't parse Priority from string " ++ show s- parseJSON value = fail $ "Couldn't parse Priority from value " ++ show value-- -- Public Functions ----------------------------------------------------------- {- |@@ -184,16 +126,21 @@ -} runMoonshine :: Moonshine a -> IO a runMoonshine (Moonshine m) = do- printBanner- MoonshineConfig {mcServerConfig} <- canteven- metricsStore <- EkgMetrics.newStore- (State {snap}, val) <- m State {- snap = return (),- metricsStore- }- let snap2 = snap <|> notFound- startServer mcServerConfig metricsStore snap2- return val+ printBanner+ MoonshineConfig {mcServerConfig} <- canteven+ metricsStore <- EkgMetrics.newStore+ (State {snap}, val) <- m State {+ snap = return (),+ metricsStore+ }+ let snap2 = snap <|> notFound+ startServer mcServerConfig metricsStore snap2+ return val+ where+ notFound :: Snap a + notFound = do+ response <- fmap (setResponseCode 404) getResponse+ finishWith response data MoonshineConfig = MoonshineConfig {@@ -210,13 +157,6 @@ {- |- Returns the user config.--}-getUserConfig :: (MonadIO io, FromJSON config) => io config-getUserConfig = liftIO canteven---{- | Like `Snap.route`, but that automatically sets up metrics for the specified routes. -}@@ -253,7 +193,7 @@ will be logged to the metrics handler. -} timed- :: T.Text+ :: Text -- ^ The name of the timer. -> Moonshine a -- ^ The action to be wrapped.@@ -266,7 +206,7 @@ {- | Make a new timer with the given name. -}-makeTimer :: T.Text -> Moonshine Timer+makeTimer :: Text -> Moonshine Timer makeTimer name = Moonshine (\state@State {metricsStore} -> do dist <- getDistribution name metricsStore return (state, T dist)@@ -294,224 +234,6 @@ {- |- Look up a parameter, and if it doesn't exist, then cause a 400- BadRequest to be returned to the user--}-requiredParam :: ByteString -> Snap ByteString-requiredParam name = do- param <- getParam name- case param of- Nothing -> badRequest ("missing parameter: " ++ d name)- Just value -> return value---{- |- Look up a parameter, and decode it using utf8. If the parameter doesn't- exist, then cause a 400 BadReqeust to be returned to the user.--}-requiredUtf8Param :: ByteString -> Snap Text-requiredUtf8Param = fmap decodeUtf8 . requiredParam---{- |- Using this will short-circuit the snap monad (in much the same way as- `fail` does for all monads), but instead the regular meaning of "fail"- in the snap monad (which allows the framework to choose some other- path), this short-circuit causes a 400 Bad Request to be returned no- matter what.--}-badRequest :: String -> Snap a-badRequest reason = do -- snap monad- writeBS (encodeUtf8 (T.pack (reason ++ "\n")))- response <- fmap (setResponseCode 400) getResponse- finishWith response---{- |- Return a @201 Created@ response.--}-created :: Snap ()-created = modifyResponse (setResponseCode 201)---{- |- Return a @204 No Content@ response.--}-noContent :: Snap ()-noContent = modifyResponse (setResponseCode 204)---{- |- Short circuit with a @404 Not Found@.--}-notFound :: Snap a -notFound = do- response <- fmap (setResponseCode 404) getResponse- finishWith response---{- |- Set the reponse code to @409 Conflict@.--}-conflict :: Snap ()-conflict = modifyResponse (setResponseCode 409)----{- |- Short circuit with a 405 Method Not Allowed. Also, set the Allow header- with the allowed methods.--}-methodNotAllowed :: [Method] -> Snap a-methodNotAllowed allowedMethods = do- response <- fmap (withAllow . setResponseCode 405) getResponse- finishWith response- where- withAllow = (- setHeader "Allow"- . encodeUtf8- . T.pack- . intercalate ", "- . map showMethod- ) allowedMethods---{- |- Asserts that the request was made using a particular HTTP request method. If- the assertion fails, then the request will result in a 405 Method Not- Allowed.--}-assertMethod :: Method -> Snap a -> Snap a-assertMethod allowedMethod snap = do- requestMethod <- getsRequest rqMethod- if requestMethod == allowedMethod- then snap- else methodNotAllowed [allowedMethod]---{- |- Write a `ToJSON` instance to the entity body, setting the @Content-Type@- header to @application/json@.--}-writeJSON :: (ToJSON json) => json -> Snap ()-writeJSON j = do- modifyResponse (setHeader "Content-Type" "application/json")- writeBS . toStrict . encode $ j---{- |- A more powerful version of writeJSON. This function writes generic- `ResponseEntity`s, and sets the right content type for them.--}-writeEntity :: (ResponseEntity e) => e -> Snap()-writeEntity e = do- modifyResponse (setHeader "Content-Type" (getContentType e))- writeBS . getBytes $ e---{- |- ContentType is an alias for ByteString--}-type ContentType = ByteString---{- |- The class of things that can be used as http response entities.--}-class ResponseEntity e where- {- |- The content type of the respone entity.- -}- getContentType :: e -> ContentType-- {- |- The bytes associated with the response entity.- -}- getBytes :: e -> ByteString---{- |- The class of things that can be read as request entitys.--}-class RequestEntity e where- {- |- Decode the entity, according to the specified content type.- -}- decodeEntity :: Maybe ContentType -> L.ByteString -> DecodeResult e---{- |- The result of trying to decode a request entity.--}-data DecodeResult e- = Unsupported- -- ^ Signifies an unsupported content type.- | BadEntity String- -- ^ Signifies that the request entity is invalid, and provides some- -- kind of reason why.- | Ok e- -- ^ Successfully decoded the entity.---{- |- Reads and decodes a request entity, returning the appropriate error- responses if the entity can't be decoded: @415 Unsupported Media Type@ if the- content type is not supported, or @400 Bad Request@ if the content type is- supported, but the entity can't be decoded.--}-readEntity :: (RequestEntity e) => Int64 -> Snap e-readEntity maxSize = do- body <- readRequestBody maxSize- contentType <- getRequestContentType- case decodeEntity contentType body of- Unsupported -> do- logUnsupported contentType- unsupportedMediaType- BadEntity reason -> do- logBadEntity contentType reason- badRequest reason- Ok e -> return e- where- getRequestContentType = getsRequest (getHeader "content-type")- logBadEntity contentType reason = warningM (- "readEntity of " ++ show contentType ++- " failed because of: " ++ show reason- )- logUnsupported contentType = warningM (- "readEntity failed because " ++ show contentType ++- " is not supported."- )---{- |- Short circuit with a 415 response.--}-unsupportedMediaType :: Snap a-unsupportedMediaType = finishWith . setResponseCode 415 =<< getResponse---{- |- Read a `FromJSON` from the request entity, causing a 400 bad request on- a parsing error. The first argument specifies the maximum size we are- willing to read. This method delegates to `Snap.Core.readRequestBody`,- so an exception will be thrown in the case where the maximum size- is exceeded.--}-readJSON :: (FromJSON json) => Int64 -> Snap json-readJSON maxSize = do- body <- readRequestBody maxSize- case eitherDecode body of- Left err -> do- logReason body err - badRequest err- Right json -> return json- where- logReason body err = warningM (- "readJSON failed because of " ++ show err ++ " on " ++ show body- )---{- | Sets the banner that moonshine should display on startup. -} setBanner :: String -> IO ()@@ -556,60 +278,16 @@ {- | Create a new gauge with the given name. -}-makeGauge :: T.Text -> Moonshine Gauge+makeGauge :: Text -> Moonshine Gauge makeGauge name = Moonshine (\state@State {metricsStore} -> do gauge <- EkgMetrics.createGauge name metricsStore return (state, G gauge) ) -{- |- Shorthand Snap action for retrieving the request method.--}-getMethod :: Snap Method-getMethod = getsRequest rqMethod---{- |- Utility method that sets the @Server:@ header to something sensible.--}-setServerVersion- :: String- -- ^ @name@ - The name of your server.- -> Version- -- ^ @version@ - The version of your server.- -> Moonshine ()-setServerVersion name =- liftSnap - . modifyResponse- . setHeader "Server"- . encodeUtf8- . T.pack- . ((name ++ "/") ++)- . showVersion-- -- Private Types -------------------------------------------------------------- {- |- Defines all the "system" config, where "system" means everything that- Moonshine knows about.--}-data SystemConfig =- SystemConfig {- logging :: Maybe LoggingConfig- , server :: Maybe ServerConfig- }--instance FromJSON SystemConfig where- parseJSON (Object topLevel) = do- logging <- topLevel .:? "logging"- server <- topLevel .:? "server"- return SystemConfig {logging, server}- parseJSON value =- fail $ "Couldn't parse system config from value " ++ show value--{- | Defines command line options. -} data Opt = Help@@ -659,7 +337,7 @@ A replacement for 'System.Remote.Monitoring.getDistribution' that uses an 'EkgMetrics.Store' instead of a 'Server'. -}-getDistribution :: T.Text -> EkgMetrics.Store -> IO Distribution+getDistribution :: Text -> EkgMetrics.Store -> IO Distribution getDistribution = EkgMetrics.createDistribution @@ -672,8 +350,8 @@ metricsStore snap = do- mapM_ startMetricsServer adminConnector- httpServe snapConfig snap+ mapM_ startMetricsServer adminConnector+ httpServe snapConfig snap where startMetricsServer :: ConnectorConfig -> IO Server@@ -686,10 +364,10 @@ snapConfig :: Config m a snapConfig = mconcat $- map toSnapConfig applicationConnector ++ [- setAccessLog (ConfigIoLog (infoM "snap" . toString)) mempty,- setErrorLog (ConfigIoLog (errorM "snap" . toString)) mempty- ]+ map toSnapConfig applicationConnector ++ [+ setAccessLog (ConfigIoLog (infoM "snap" . toString)) mempty,+ setErrorLog (ConfigIoLog (errorM "snap" . toString)) mempty+ ] where toString = T.unpack . decodeUtf8 toSnapConfig :: ConnectorConfig -> Config m a@@ -722,47 +400,9 @@ {- |- Make sure that the path info is empty. If it isn't, then fail the snap action- using `pass`. This is helpful when using routes to make sure that a route- named "/foo" does not match an request uri of "/foo/bar"--}-exactPath :: Snap a -> Snap a-exactPath s = do- pathInfo <- getsRequest rqPathInfo- liftIO $ debugM ("PathInfo: " ++ show pathInfo)- case pathInfo of- "" -> s- _ -> pass---{- |- decode a bytestring to a string, via utf8--}-d :: ByteString -> String-d = T.unpack . decodeUtf8---{- |- The default `show` implementation of `Method` isn't good enough,- specifically the value `show (Method "FOO")` == `"Method \"Foo\""`, which is- unsuitable for use when displaying the actual name of the method in a formal- context (read: in the `Allow` header).--}-showMethod :: Method -> String-showMethod (Method m) = (T.unpack . decodeUtf8) m-showMethod m = show m---{- | Shorthand logging -} debugM :: String -> IO () debugM = L.debugM "moonshine" --{- |- Shorthand logging--}-warningM :: (MonadIO io) => String -> io ()-warningM = liftIO . L.warningM "moonshine"