Loading api/Main.hs +4 −19 Original line number Diff line number Diff line {-# LANGUAGE OverloadedStrings #-} module Main (main) where import Control.Monad.IO.Class (liftIO) import GHC.Exts (fromList) import Web.Scotty import Data.Aeson (Value(..), toJSON, object, (.=)) import RD.Lib (sha1sum) import App (app) main :: IO () main = scotty 8082 $ do get "/" $ do json $ Object $ fromList [("ok", Bool True) ,("app", "reliable-download api")] get "/debug/t1" $ do sha1 <- liftIO $ sha1sum "/home/sylecn/persist/cache/ideaIC-2018.1.tar.gz" json $ object ["ok" .= True ,"sha1sum" .= sha1] get "/:word" $ do beam <- param "word" sha1 <- liftIO $ sha1sum beam html $ mconcat ["sha1sum for ", beam, " is ", sha1, "."] main = do putStrLn "listening on 0.0.0.0:8082" scotty 8082 app operational +95 −0 Original line number Diff line number Diff line Loading @@ -3,8 +3,29 @@ Time-stamp: <2018-05-05> #+STARTUP: content * notes :entry: ** 2018-05-05 writing tests in hspec - Test WAI application using hspec hspec/hspec-wai: Helpers to test WAI application with Hspec https://github.com/hspec/hspec-wai Here has example. good. Hspec: A Testing Framework for Haskell http://hspec.github.io/ - Test.Hspec.Expectations https://hackage.haskell.org/package/hspec-expectations-0.8.2/docs/Test-Hspec-Expectations.html#v:Expectation shouldBe shouldStartWith shouldEndWith shouldContain etc ** 2018-05-05 Data.Aeson http://hackage.haskell.org/package/aeson-1.3.1.0/docs/Data-Aeson.html // this seems more readable than lts haskell's doc. Aeson: the tutorial https://artyom.me/aeson ** 2018-05-05 sol/hpack: hpack: An alternative format for Haskell packages https://github.com/sol/hpack ** 2018-05-04 make hoogle work in current project Loading Loading @@ -36,9 +57,42 @@ scotty/examples at master · scotty-web/scotty https://github.com/scotty-web/scotty/tree/master/examples ** 2018-05-04 for project notes, see GTD.org id002 ** 2018-05-05 API spec - POST /rd json body {"path": "/path/to/resource"} - * later :entry: ** 2018-05-05 should rd-api serve the static file itself? that way you don't need to tell rd-server the web root dir. - normally nginx serve static files under $WEB_ROOT rd-server --web-root $WEB_ROOT this serve /rd/.*, provides metadata for files. - it may be faster if you just do rd-server --web-root $WEB_ROOT This serves both /rd/.* and /.* * current :entry: ** ** 2018-05-05 utf-8 character not working well. curl http://localhost:8082/rd/%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.rar {"ok":true,"path":"中"} only first character is in path key. - a scotty bug? full_path is also wrong. not regexp problem. - well, since this encoding doesn't work well. I think I will pass the path in json body instead of in the URL. I don't need it in the URL anyway. download must be handled by a rd client. - search: haskell scotty path utf-8 character check source code. ** 2018-05-04 check whether haskell is viable for this project. In static file hosting nginx reverse proxy /rd/ to rd application server. Loading @@ -63,4 +117,45 @@ location /rd/ { - * done :entry: ** 2018-05-05 how to test scotty web api wai-test: Unit test framework (built on HUnit) for WAI applications. (deprecated) http://hackage.haskell.org/package/wai-test merged to wai-extra wai-extra: Provides some basic WAI handlers and middleware. http://hackage.haskell.org/package/wai-extra hspec/hspec-wai: Helpers to test WAI application with Hspec https://github.com/hspec/hspec-wai Here has example. good. Hspec: A Testing Framework for Haskell http://hspec.github.io/ - can I write test without those describe and it strings? no. - how to check some part of the response body? requires a WaiExpectation in the do block. type WaiExpectation = WaiSession () shouldRespondWith (get "/rd/") 200 source code https://hackage.haskell.org/package/hspec-wai-0.9.0/docs/src/Test-Hspec-Wai.html#shouldRespondWith shouldBe :: (HasCallStack, Show a, Eq a) => a -> a -> Expectation I can't use shouldBe there. because I need a WaiExpectation. This worked: resp <- get "/rd/" liftIO (simpleStatus resp `shouldBe` status200) ** 2018-05-05 can I start scotty web server and stop it in unit test? nope. it returns IO (), there is no PID or something. I can use haskell fork though. ** 2018-05-05 can I pattern match on URL with / in them? yes. see Route Patterns. * wontfix :entry: package.yaml +17 −1 Original line number Diff line number Diff line Loading @@ -14,9 +14,11 @@ extra-source-files: default-extensions: - OverloadedStrings - ScopedTypeVariables dependencies: - base >= 4.7 && < 5 - wai - scotty - cryptohash - bytestring Loading @@ -38,8 +40,22 @@ executables: main: Main.hs tests: api-test: main: TestApi.hs source-dirs: test dependencies: - reliable-download - HUnit - hspec - hspec-wai - http-types - wai-extra - unordered-containers - binary lib-test: main: TestLib.hs main: test.hs other-modules: - TestLib source-dirs: test dependencies: - reliable-download Loading src/App.hs 0 → 100644 +29 −0 Original line number Diff line number Diff line module App (app, waiApp) where import Control.Monad.IO.Class (liftIO) import Network.Wai (Application) import Web.Scotty import Data.Aeson (Value(..), toJSON, object, (.=)) import qualified Data.Text.Lazy as LT import RD.Lib (sha1sum) app :: ScottyM () app = do get (literal "/rd/") $ do json $ object [("ok" .= True) ,("app" .= ("reliable-download api" :: String))] get (regex "^/rd/(.*)") $ do path :: LT.Text <- param "1" fullPath :: LT.Text <- param "0" json $ object [("ok" .= True) ,("full_path" .= fullPath) ,("path" .= path)] get "/rd/debug/t1" $ do sha1 <- liftIO $ sha1sum "/home/sylecn/persist/cache/ideaIC-2018.1.tar.gz" json $ object ["ok" .= True ,"sha1sum" .= sha1] waiApp :: IO Application waiApp = scottyApp app test/TestApi.hs 0 → 100644 +105 −0 Original line number Diff line number Diff line -- module TestApi (main) where -- GET /rd/some_complicated_url_safe_text import Test.Hspec import Test.Hspec.Wai import Network.Wai.Test import Network.HTTP.Types.Status import Network.HTTP.Types.URI (encodePathSegments, decodePathSegments) import Data.Binary.Builder (toLazyByteString) import qualified Data.Aeson as J import qualified Data.ByteString.Lazy as LB import qualified Data.Text as T import qualified Data.HashMap.Strict as H import App (waiApp) -- | decode response as json object. jsonObject :: SResponse -> Maybe J.Object jsonObject resp = J.decode $ simpleBody resp -- | decode response as json object, then retrieve a key. jsonKey :: SResponse -> T.Text -> Maybe J.Value jsonKey resp key = do map <- jsonObject resp H.lookup key map jsonKeyAsBool :: SResponse -> T.Text -> Maybe Bool jsonKeyAsBool resp key = do v <- jsonKey resp key case v of J.Bool b -> Just b _ -> Nothing jsonKeyAsText :: SResponse -> T.Text -> Maybe T.Text jsonKeyAsText resp key = do v <- jsonKey resp key case v of J.String b -> Just b _ -> Nothing spec :: Spec spec = do describe "dumb" $ do it "should be that" $ do True `shouldBe` True describe "3rd party libs" $ do it "should encode and decode utf-8 characters in URL" $ do ((decodePathSegments . LB.toStrict . toLazyByteString . encodePathSegments) ["中文1", "路径2"]) `shouldBe` ["中文1", "路径2"] -- | like get, but accept path in [T.Text] format and do url safe encoding. getPath :: [T.Text] -> WaiSession SResponse getPath = get . LB.toStrict . toLazyByteString . encodePathSegments apiSpec :: Spec apiSpec = with waiApp $ do describe "rd api" $ do it "has health check" $ do resp <- get "/rd/" liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) -- liftIO (shouldSatisfy 1 (\x -> True)) liftIO ((fmap (T.isInfixOf "reliable-download") (jsonKeyAsText resp "app")) `shouldBe` Just True) it "only respond with /rd/ prefix" $ do get "/" `shouldRespondWith` 404 get "/abc" `shouldRespondWith` 404 it "should parse basic path correctly" $ do resp <- get "/rd/abc" liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc") resp <- get "/rd/abc/def" liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc/def") resp <- get "/rd/abc/def/" liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc/def/") it "should parse complex path correctly" $ do resp <- getPath ["rd", "abc def # ? ghi"] liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc def # ? ghi") resp <- getPath ["rd", "abc/def.jpg"] liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc/def.jpg") -- resp <- getPath ["rd", "中文文件名.rar"] -- liftIO (simpleStatus resp `shouldBe` status200) -- liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) -- liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "中文文件名.rar") main :: IO () main = do hspec spec hspec apiSpec Loading
api/Main.hs +4 −19 Original line number Diff line number Diff line {-# LANGUAGE OverloadedStrings #-} module Main (main) where import Control.Monad.IO.Class (liftIO) import GHC.Exts (fromList) import Web.Scotty import Data.Aeson (Value(..), toJSON, object, (.=)) import RD.Lib (sha1sum) import App (app) main :: IO () main = scotty 8082 $ do get "/" $ do json $ Object $ fromList [("ok", Bool True) ,("app", "reliable-download api")] get "/debug/t1" $ do sha1 <- liftIO $ sha1sum "/home/sylecn/persist/cache/ideaIC-2018.1.tar.gz" json $ object ["ok" .= True ,"sha1sum" .= sha1] get "/:word" $ do beam <- param "word" sha1 <- liftIO $ sha1sum beam html $ mconcat ["sha1sum for ", beam, " is ", sha1, "."] main = do putStrLn "listening on 0.0.0.0:8082" scotty 8082 app
operational +95 −0 Original line number Diff line number Diff line Loading @@ -3,8 +3,29 @@ Time-stamp: <2018-05-05> #+STARTUP: content * notes :entry: ** 2018-05-05 writing tests in hspec - Test WAI application using hspec hspec/hspec-wai: Helpers to test WAI application with Hspec https://github.com/hspec/hspec-wai Here has example. good. Hspec: A Testing Framework for Haskell http://hspec.github.io/ - Test.Hspec.Expectations https://hackage.haskell.org/package/hspec-expectations-0.8.2/docs/Test-Hspec-Expectations.html#v:Expectation shouldBe shouldStartWith shouldEndWith shouldContain etc ** 2018-05-05 Data.Aeson http://hackage.haskell.org/package/aeson-1.3.1.0/docs/Data-Aeson.html // this seems more readable than lts haskell's doc. Aeson: the tutorial https://artyom.me/aeson ** 2018-05-05 sol/hpack: hpack: An alternative format for Haskell packages https://github.com/sol/hpack ** 2018-05-04 make hoogle work in current project Loading Loading @@ -36,9 +57,42 @@ scotty/examples at master · scotty-web/scotty https://github.com/scotty-web/scotty/tree/master/examples ** 2018-05-04 for project notes, see GTD.org id002 ** 2018-05-05 API spec - POST /rd json body {"path": "/path/to/resource"} - * later :entry: ** 2018-05-05 should rd-api serve the static file itself? that way you don't need to tell rd-server the web root dir. - normally nginx serve static files under $WEB_ROOT rd-server --web-root $WEB_ROOT this serve /rd/.*, provides metadata for files. - it may be faster if you just do rd-server --web-root $WEB_ROOT This serves both /rd/.* and /.* * current :entry: ** ** 2018-05-05 utf-8 character not working well. curl http://localhost:8082/rd/%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.rar {"ok":true,"path":"中"} only first character is in path key. - a scotty bug? full_path is also wrong. not regexp problem. - well, since this encoding doesn't work well. I think I will pass the path in json body instead of in the URL. I don't need it in the URL anyway. download must be handled by a rd client. - search: haskell scotty path utf-8 character check source code. ** 2018-05-04 check whether haskell is viable for this project. In static file hosting nginx reverse proxy /rd/ to rd application server. Loading @@ -63,4 +117,45 @@ location /rd/ { - * done :entry: ** 2018-05-05 how to test scotty web api wai-test: Unit test framework (built on HUnit) for WAI applications. (deprecated) http://hackage.haskell.org/package/wai-test merged to wai-extra wai-extra: Provides some basic WAI handlers and middleware. http://hackage.haskell.org/package/wai-extra hspec/hspec-wai: Helpers to test WAI application with Hspec https://github.com/hspec/hspec-wai Here has example. good. Hspec: A Testing Framework for Haskell http://hspec.github.io/ - can I write test without those describe and it strings? no. - how to check some part of the response body? requires a WaiExpectation in the do block. type WaiExpectation = WaiSession () shouldRespondWith (get "/rd/") 200 source code https://hackage.haskell.org/package/hspec-wai-0.9.0/docs/src/Test-Hspec-Wai.html#shouldRespondWith shouldBe :: (HasCallStack, Show a, Eq a) => a -> a -> Expectation I can't use shouldBe there. because I need a WaiExpectation. This worked: resp <- get "/rd/" liftIO (simpleStatus resp `shouldBe` status200) ** 2018-05-05 can I start scotty web server and stop it in unit test? nope. it returns IO (), there is no PID or something. I can use haskell fork though. ** 2018-05-05 can I pattern match on URL with / in them? yes. see Route Patterns. * wontfix :entry:
package.yaml +17 −1 Original line number Diff line number Diff line Loading @@ -14,9 +14,11 @@ extra-source-files: default-extensions: - OverloadedStrings - ScopedTypeVariables dependencies: - base >= 4.7 && < 5 - wai - scotty - cryptohash - bytestring Loading @@ -38,8 +40,22 @@ executables: main: Main.hs tests: api-test: main: TestApi.hs source-dirs: test dependencies: - reliable-download - HUnit - hspec - hspec-wai - http-types - wai-extra - unordered-containers - binary lib-test: main: TestLib.hs main: test.hs other-modules: - TestLib source-dirs: test dependencies: - reliable-download Loading
src/App.hs 0 → 100644 +29 −0 Original line number Diff line number Diff line module App (app, waiApp) where import Control.Monad.IO.Class (liftIO) import Network.Wai (Application) import Web.Scotty import Data.Aeson (Value(..), toJSON, object, (.=)) import qualified Data.Text.Lazy as LT import RD.Lib (sha1sum) app :: ScottyM () app = do get (literal "/rd/") $ do json $ object [("ok" .= True) ,("app" .= ("reliable-download api" :: String))] get (regex "^/rd/(.*)") $ do path :: LT.Text <- param "1" fullPath :: LT.Text <- param "0" json $ object [("ok" .= True) ,("full_path" .= fullPath) ,("path" .= path)] get "/rd/debug/t1" $ do sha1 <- liftIO $ sha1sum "/home/sylecn/persist/cache/ideaIC-2018.1.tar.gz" json $ object ["ok" .= True ,"sha1sum" .= sha1] waiApp :: IO Application waiApp = scottyApp app
test/TestApi.hs 0 → 100644 +105 −0 Original line number Diff line number Diff line -- module TestApi (main) where -- GET /rd/some_complicated_url_safe_text import Test.Hspec import Test.Hspec.Wai import Network.Wai.Test import Network.HTTP.Types.Status import Network.HTTP.Types.URI (encodePathSegments, decodePathSegments) import Data.Binary.Builder (toLazyByteString) import qualified Data.Aeson as J import qualified Data.ByteString.Lazy as LB import qualified Data.Text as T import qualified Data.HashMap.Strict as H import App (waiApp) -- | decode response as json object. jsonObject :: SResponse -> Maybe J.Object jsonObject resp = J.decode $ simpleBody resp -- | decode response as json object, then retrieve a key. jsonKey :: SResponse -> T.Text -> Maybe J.Value jsonKey resp key = do map <- jsonObject resp H.lookup key map jsonKeyAsBool :: SResponse -> T.Text -> Maybe Bool jsonKeyAsBool resp key = do v <- jsonKey resp key case v of J.Bool b -> Just b _ -> Nothing jsonKeyAsText :: SResponse -> T.Text -> Maybe T.Text jsonKeyAsText resp key = do v <- jsonKey resp key case v of J.String b -> Just b _ -> Nothing spec :: Spec spec = do describe "dumb" $ do it "should be that" $ do True `shouldBe` True describe "3rd party libs" $ do it "should encode and decode utf-8 characters in URL" $ do ((decodePathSegments . LB.toStrict . toLazyByteString . encodePathSegments) ["中文1", "路径2"]) `shouldBe` ["中文1", "路径2"] -- | like get, but accept path in [T.Text] format and do url safe encoding. getPath :: [T.Text] -> WaiSession SResponse getPath = get . LB.toStrict . toLazyByteString . encodePathSegments apiSpec :: Spec apiSpec = with waiApp $ do describe "rd api" $ do it "has health check" $ do resp <- get "/rd/" liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) -- liftIO (shouldSatisfy 1 (\x -> True)) liftIO ((fmap (T.isInfixOf "reliable-download") (jsonKeyAsText resp "app")) `shouldBe` Just True) it "only respond with /rd/ prefix" $ do get "/" `shouldRespondWith` 404 get "/abc" `shouldRespondWith` 404 it "should parse basic path correctly" $ do resp <- get "/rd/abc" liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc") resp <- get "/rd/abc/def" liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc/def") resp <- get "/rd/abc/def/" liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc/def/") it "should parse complex path correctly" $ do resp <- getPath ["rd", "abc def # ? ghi"] liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc def # ? ghi") resp <- getPath ["rd", "abc/def.jpg"] liftIO (simpleStatus resp `shouldBe` status200) liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "abc/def.jpg") -- resp <- getPath ["rd", "中文文件名.rar"] -- liftIO (simpleStatus resp `shouldBe` status200) -- liftIO ((jsonKeyAsBool resp "ok") `shouldBe` Just True) -- liftIO ((jsonKeyAsText resp "path") `shouldBe` Just "中文文件名.rar") main :: IO () main = do hspec spec hspec apiSpec