scotty-fay (empty) → 0.0.1
raw patch · 11 files changed
+443/−0 lines, 11 filesdep +HUnitdep +basedep +bytestringsetup-changed
Dependencies added: HUnit, base, bytestring, data-default, directory, fay, fay-jquery, http-types, scotty, scotty-fay, test-framework, test-framework-hunit, text, transformers, wai, wai-test
Files
- LICENSE +25/−0
- README.md +12/−0
- Setup.hs +4/−0
- example/Main.hs +24/−0
- example/src/fay/HelloWorld.hs +11/−0
- scotty-fay.cabal +62/−0
- src/Web/Scotty/Fay.hs +52/−0
- src/Web/Scotty/Fay/Config.hs +41/−0
- src/Web/Scotty/Fay/Internal.hs +89/−0
- src/Web/Scotty/Fay/Utils.hs +17/−0
- test/Main.hs +106/−0
+ LICENSE view
@@ -0,0 +1,25 @@++The MIT License (MIT)++Copyright (c) 2013 Harry Garrood++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.+++
+ README.md view
@@ -0,0 +1,12 @@+scotty-fay+==========++A Scotty application which compiles and serves [Fay][] on request.++Todo:++* Production mode. Precompile Fay.++See the example/ directory for example use.++[Fay]: http://fay-lang.org
+ Setup.hs view
@@ -0,0 +1,4 @@+import qualified Distribution.Simple++main :: IO ()+main = Distribution.Simple.defaultMain
+ example/Main.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings #-}+import qualified Data.Text.Lazy as T+import Web.Scotty+import Web.Scotty.Fay++main :: IO ()+main = scotty 3000 $ do+ serveFay $+ -- If the first segment of the request path matches this, try to serve+ -- Fay. Otherwise try the next route.+ under "/scotty-fay" .+ -- Specify the directory where your Fay files are.+ from "src/fay"++ get "/" $ do+ let (+>) = T.append -- just to make it shorter+ html $+ "<!doctype html>" +>+ "<html>" +>+ "<head>" +>+ "<script type=text/javascript src=/scotty-fay/HelloWorld.hs></script>" +>+ "</head>" +>+ "<body><h1>lol</h1></body>" +>+ "</html>"
+ example/src/fay/HelloWorld.hs view
@@ -0,0 +1,11 @@+-- src/fay/HelloWorld.hs+module HelloWorld where++import Prelude+import FFI++alert :: String -> Fay ()+alert = ffi "alert(%1)"++main :: Fay ()+main = alert "hello, world"
+ scotty-fay.cabal view
@@ -0,0 +1,62 @@+name: scotty-fay+version: 0.0.1+build-type: Simple+cabal-version: >= 1.10++category: Development, Web, Fay+maintainer: Harry Garrood <harry@garrood.me>+license: MIT+license-file: LICENSE+extra-source-files:+ README.md+ example/Main.hs+ example/src/fay/HelloWorld.hs++synopsis: Fay integration for Scotty.+description: Fay integration for Scotty. For more information, please see <https://github.com/hdgarrood/scotty-fay>.++source-repository head+ type: git+ location: https://github.com/hdgarrood/scotty-fay++library+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall+ default-extensions: OverloadedStrings, Rank2Types+ exposed-modules: Web.Scotty.Fay+ other-modules: Web.Scotty.Fay.Utils,+ Web.Scotty.Fay.Internal,+ Web.Scotty.Fay.Config+ build-depends: base >= 4.0 && < 5.0,+ scotty >= 0.6.0 && < 0.7.0,+ fay >= 0.18 && < 0.19,+ fay-jquery,+ bytestring,+ data-default,+ http-types,+ wai,+ transformers,+ text,+ directory++test-suite scotty-fay-tests+ type: exitcode-stdio-1.0+ main-is: Main.hs+ hs-source-dirs: test+ default-language: Haskell2010+ ghc-options: -Wall+ default-extensions: OverloadedStrings,+ Rank2Types+ build-depends: base,+ scotty-fay,+ wai-test,+ test-framework,+ test-framework-hunit,+ HUnit,+ directory,+ wai,+ transformers,+ http-types,+ scotty+
+ src/Web/Scotty/Fay.hs view
@@ -0,0 +1,52 @@+module Web.Scotty.Fay+ ( module Web.Scotty.Fay.Config+ , compileFile+ , serveFay+ , serveFay'+ ) where++import Control.Monad.IO.Class (MonadIO, liftIO)+import qualified Data.Text.Lazy as LT+import Web.Scotty.Trans hiding (file)+import qualified Fay+import System.Directory++import Web.Scotty.Fay.Internal+import Web.Scotty.Fay.Config++data CompileResult = Success String+ | Error String+ | FileNotFound String++compileFile :: Config -> FilePath -> IO CompileResult+compileFile conf file = do+ exists <- doesFileExist file+ if not exists+ then return . FileNotFound $+ "scotty-fay: Could not find " ++ file -- TODO: hs relative path?+ else do+ file' <- canonicalizePath file+ res <- Fay.compileFile (toFay conf) file'+ case res of+ Right (out, _) -> return $ Success out+ Left err -> return . Error . Fay.showCompileError $ err++serveFay :: (MonadIO a, Functor a) => ConfigBuilder -> ScottyT LT.Text a ()+serveFay = serveFay' . buildConfig++serveFay' :: (MonadIO a, Functor a) => Config -> ScottyT LT.Text a ()+serveFay' conf = do+ liftIO $ initialize conf++ get (pattern $ configBasePath conf) $ do+ path <- maybeParam "path"+ case path of+ Just path' -> do+ let filePath = configSrcDir conf ++ "/" ++ path'+ result <- liftIO (compileFile conf filePath)+ case result of+ Success code -> respondWithJs code+ Error err -> raiseErr err+ FileNotFound msg -> notFoundMsg msg+ Nothing -> notFoundMsg "scotty-fay: requested path is invalid"+ where
+ src/Web/Scotty/Fay/Config.hs view
@@ -0,0 +1,41 @@+module Web.Scotty.Fay.Config+ ( Config+ , ConfigBuilder+ , configSrcDir+ , configBasePath+ , buildConfig+ , under+ , from+ , toFay+ ) where++import Data.Default+import qualified Data.Text as T+import qualified Fay+import qualified Fay.Compiler.Config as Fay++data Config = Config+ { configBasePath :: T.Text+ , configSrcDir :: FilePath+ }++instance Default Config where+ def = Config+ { configBasePath = ""+ , configSrcDir = ""+ }++type ConfigBuilder = Config -> Config++buildConfig :: ConfigBuilder -> Config+buildConfig f = f def++-- Convert a scotty-fay Config to a Fay CompileConfig+toFay :: Config -> Fay.CompileConfig+toFay conf = Fay.addConfigDirectoryIncludePaths [configSrcDir conf] $ def++under :: T.Text -> ConfigBuilder+under basePath conf = conf { configBasePath = basePath }++from :: FilePath -> ConfigBuilder+from dir conf = conf { configSrcDir = dir }
+ src/Web/Scotty/Fay/Internal.hs view
@@ -0,0 +1,89 @@+module Web.Scotty.Fay.Internal where++import Control.Monad+import Control.Monad.IO.Class+import Data.Maybe+import qualified Data.Text as T+import qualified Data.Text.Lazy as LT+import Web.Scotty.Trans+import Network.Wai+import Network.HTTP.Types (notFound404)+import System.Directory++import Web.Scotty.Fay.Config+import Web.Scotty.Fay.Utils+++-- General dumping ground for helper functions.++warn :: String -> IO ()+warn = putStrLn . ("scotty-fay: " ++)++-- Run once, at application startup. Used for checking that the configuration+-- is sane.+initialize :: Config -> IO ()+initialize conf = do+ let srcDir = configSrcDir conf+ exists <- doesDirectoryExist srcDir+ unless exists $ do+ warn $ "The source directory: " ++ show srcDir +++ " does not exist. Continuing anyway..."++-- The route matcher function, to be given to Scotty. Ensure the request path+-- starts with the given base path, and then put the rest of the path into+-- the "filePath" parameter (if it exists, and is valid).+route :: T.Text -> Request -> Maybe [Param]+route base req = do+ base' <- eatFromStart "/" base+ let pathSegments = pathInfo req+ first <- safeHead pathSegments+ rest <- nonEmptyTail pathSegments++ guard (first == base')+ return . maybeToList . makeParam $ rest+ where+ safeHead = listToMaybe++ nonEmptyTail [] = Nothing+ nonEmptyTail [_] = Nothing+ nonEmptyTail (_:xs) = Just xs++pattern :: T.Text -> RoutePattern+pattern = function . route++eatFromStart :: T.Text -> T.Text -> Maybe T.Text+eatFromStart prefix str =+ if prefix `T.isPrefixOf` str+ then Just $ T.drop (T.length prefix) str+ else Nothing++makeParam :: [T.Text] -> Maybe (LT.Text, LT.Text)+makeParam = fmap (\p -> ("path", LT.fromStrict p)) . secureRejoin++-- Rejoin path segments to get a file path, while preventing directory+-- traversal attacks.+secureRejoin :: [T.Text] -> Maybe T.Text+secureRejoin xs = do+ guard (noDots xs)+ return $ T.intercalate "/" xs+ where+ noDots = not . elem ".."++maybeParam :: (Functor a, MonadIO a, Parsable b) =>+ LT.Text -> ActionT LT.Text a (Maybe b)+maybeParam key = fmap Just (param key) `rescue` (const $ return Nothing)++respondWithJs :: MonadIO a => String -> ActionT LT.Text a ()+respondWithJs jsString = do+ setHeader "Content-Type" "text/javascript"+ raw $ stringToLazyByteString jsString++notFoundMsg :: MonadIO a => String -> ActionT LT.Text a ()+notFoundMsg msg = do+ status notFound404+ text (stringToLazyText msg)++raiseErr :: MonadIO a => String -> ActionT LT.Text a ()+raiseErr = raise . wrapInPreTag . stringToLazyText+ where+ wrapInPreTag str = "<pre>" `LT.append` str `LT.append` "</pre>"
+ src/Web/Scotty/Fay/Utils.hs view
@@ -0,0 +1,17 @@+module Web.Scotty.Fay.Utils where++import qualified Data.ByteString.Lazy as LB+import qualified Data.ByteString.Lazy.Char8 as LBC8+import qualified Data.ByteString as B+import qualified Data.Text.Lazy as LT+import qualified Data.Text.Lazy.Encoding as LTE+import qualified Data.Text.Encoding as TE++stringToLazyByteString :: String -> LB.ByteString+stringToLazyByteString = LBC8.pack++stringToLazyText :: String -> LT.Text+stringToLazyText = LTE.decodeUtf8 . stringToLazyByteString++strictByteStringToLazyText :: B.ByteString -> LT.Text+strictByteStringToLazyText = LT.fromStrict . TE.decodeUtf8
+ test/Main.hs view
@@ -0,0 +1,106 @@+module Main where++import Control.Monad.IO.Class (liftIO)+import Web.Scotty hiding (request)+import Web.Scotty.Fay+import qualified Network.HTTP.Types as HTTP+import Network.Wai+import Network.Wai.Test+import Test.Framework+import Test.Framework.Providers.HUnit+import qualified Test.HUnit.Base as H+import System.Directory++main :: IO ()+main = do+ dir <- getCurrentDirectory+ putStrLn dir+ tests >>= defaultMain++tests :: IO [Test]+tests = sequence+ [ waiTest "compiles Fay" test_compilesFay+ , waiTest "serveFay captures everything under base" test_capturesEverything+ , waiTest "imports" test_imports+ , waiTest "directory traversal" test_directoryTraversal+ , return $ testGroup "configuration" $+ [ testCase "configuring src dir" test_configuringSrcDir+ , testCase "configuring base path" test_configuringBasePath+ ]+ ]++waiTest :: String -> Session () -> IO Test+waiTest name session = do+ app' <- scottyApp app+ return $ testCase name $ runSession session app'++app :: ScottyM ()+app = do+ serveFay (under "/fay" . from "test/fay-resources")++ get "/" $ do+ text "this is the root"++ get "/fay/shouldnt-get-here" $ do+ text "it shouldn't get here"++-- This is handy to have when debugging interactively.+runScottyApp :: IO ()+runScottyApp = scotty 3000 app++assertBool :: String -> Bool -> Session ()+assertBool str p = liftIO $ H.assertBool str p++assertNotStatus :: Int -> SResponse -> Session ()+assertNotStatus i SResponse{simpleStatus = s} = assertBool (concat+ [ "Expected a status other than "+ , show i+ ]) $ i /= sc+ where+ sc = HTTP.statusCode s++assertJavaScriptRenderedOk :: SResponse -> Session ()+assertJavaScriptRenderedOk response = do+ assertStatus 200 response+ assertHeader "Content-Type" "text/javascript" response++test_compilesFay :: Session ()+test_compilesFay = do+ let req = setPath defaultRequest "/fay/Fact.hs"+ resp <- request req++ assertJavaScriptRenderedOk resp++test_capturesEverything :: Session ()+test_capturesEverything = do+ let req = setPath defaultRequest "/fay/shouldnt-get-here"+ resp <- request req++ assertNotStatus 200 resp++test_imports :: Session ()+test_imports = do+ let req = setPath defaultRequest "/fay/Fib.hs"+ resp <- request req++ assertJavaScriptRenderedOk resp++test_directoryTraversal :: Session ()+test_directoryTraversal = do+ let req = setPath defaultRequest "/fay/test/Fib/../Fib.hs"+ resp <- request req++ assertNotStatus 200 resp++assertEq :: (Eq a, Show a) => a -> a -> H.Assertion+assertEq = H.assertEqual ""++test_configuringSrcDir :: H.Assertion+test_configuringSrcDir =+ assertEq "src" $+ (configSrcDir . buildConfig $ (under "/js" . from "src"))++test_configuringBasePath :: H.Assertion+test_configuringBasePath =+ assertEq "/js" $+ (configBasePath . buildConfig $ (under "/js" . from "src"))