diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c4b4f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/node_modules/ +/bower_components/ +/tmp/ +/dist/ +/output/ +/coverage/ +public/*.js +.psci_modules +.psci \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..919962f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: node_js +node_js: + - 0.10 +env: + - TAG=v0.6.8 +install: + - wget -O $HOME/purescript.tar.gz https://github.com/purescript/purescript/releases/download/$TAG/linux64.tar.gz + - sudo tar zxvf $HOME/purescript.tar.gz -C /usr/local/bin purescript/psc{,i,-docs,-make} --strip-components=1 + - sudo chmod a+x /usr/local/bin/psc{,i,-docs,-make} + - npm install bower gulp -g + - npm install && bower install +script: + - gulp bundle-test diff --git a/MODULES.md b/MODULES.md new file mode 100644 index 0000000..f49c346 --- /dev/null +++ b/MODULES.md @@ -0,0 +1,209 @@ +# Module Documentation + +## Module Routing + +#### `hashChanged` + +``` purescript +hashChanged :: forall e. (String -> String -> Eff e Unit) -> Eff e Unit +``` + + +#### `hashes` + +``` purescript +hashes :: forall e. (String -> String -> Eff e Unit) -> Eff e Unit +``` + + +#### `matches` + +``` purescript +matches :: forall e a. Match a -> (Maybe a -> a -> Eff e Unit) -> Eff e Unit +``` + + +#### `matchHash` + +``` purescript +matchHash :: forall a. Match a -> String -> Either String a +``` + + + +## Module Routing.Match + +#### `Match` + +``` purescript +newtype Match a + = Match (Route -> Either String (Tuple Route a)) +``` + + +#### `matchMatchClass` + +``` purescript +instance matchMatchClass :: MatchClass Match +``` + + +#### `matchFunctor` + +``` purescript +instance matchFunctor :: Functor Match +``` + + +#### `matchAlt` + +``` purescript +instance matchAlt :: Alt Match +``` + + +#### `matchPlus` + +``` purescript +instance matchPlus :: Plus Match +``` + + +#### `matchAlternative` + +``` purescript +instance matchAlternative :: Alternative Match +``` + + +#### `matchApply` + +``` purescript +instance matchApply :: Apply Match +``` + + +#### `matchApplicative` + +``` purescript +instance matchApplicative :: Applicative Match +``` + + +#### `matchBind` + +``` purescript +instance matchBind :: Bind Match +``` + + +#### `matchMonad` + +``` purescript +instance matchMonad :: Monad Match +``` + + +#### `matchMonadPlus` + +``` purescript +instance matchMonadPlus :: MonadPlus Match +``` + + +#### `runMatch` + +``` purescript +runMatch :: forall a. Match a -> Route -> Either String a +``` + + + +## Module Routing.Parser + +#### `parse` + +``` purescript +parse :: String -> Route +``` + + + +## Module Routing.Setter + +#### `setHash` + +``` purescript +setHash :: forall e. String -> Eff e Unit +``` + + +#### `RouteState` + +``` purescript +class RouteState a where + toHash :: a -> String +``` + +Class of types that can be converted to hashes + +#### `setRouteState` + +``` purescript +setRouteState :: forall r e. (RouteState r) => r -> Eff e Unit +``` + +wrapper over `setHash` that uses `RouteState` + + +## Module Routing.Types + +#### `RoutePart` + +``` purescript +data RoutePart + = Path String + | Query (M.StrMap String) +``` + + +#### `Route` + +``` purescript +type Route = [RoutePart] +``` + + + +## Module Routing.Match.Class + +#### `MatchClass` + +``` purescript +class (MonadPlus f) <= MatchClass f where + lit :: String -> f Unit + var :: f String + param :: String -> f String + fail :: forall a. String -> f a +``` + + + +## Module Routing.Match.Combinators + +#### `num` + +``` purescript +num :: forall f. (MatchClass f) => String -> f Number +``` + + +#### `bool` + +``` purescript +bool :: forall f. (MatchClass f) => String -> f Boolean +``` + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b099750 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# purescript-routing + +[![Build Status](https://travis-ci.org/cryogenian/purescript-routing.svg?branch=master)](https://travis-ci.org/cryogenian/purescript-routing) + diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..fbecab6 --- /dev/null +++ b/bower.json @@ -0,0 +1,35 @@ +{ + "name": "purescript-routing", + "homepage": "https://github.com/slamdata/purescript-routing", + "authors": [ + "Maxim Zimaliev " + ], + "description": "purescript library for routing", + "keywords": [ + "purescript", + "routing" + ], + "license": "Apache 2.0", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "purescript-strings": "~0.4.3", + "purescript-maps": "~0.3.2", + "purescript-control": "~0.2.2", + "purescript-transformers": "~0.5.1", + "purescript-arrays": "~0.3.3", + "purescript-monoid": "~0.2.0", + "purescript-validation": "~0.1.1", + "purescript-semirings": "~0.1.1" + }, + "devDependencies": { + "purescript-timers": "~0.0.8", + "purescript-debug-foreign": "~0.0.4", + "purescript-globals": "~0.1.5" + } +} diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..f8dbeba --- /dev/null +++ b/gulpfile.js @@ -0,0 +1 @@ +require("mandragora-bucket").define(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..4c6a38b --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "purescript-routing", + "description": "Routing lib for purescript", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/slamdata/purescript-routing.git" + }, + "keywords": [ + "purescript", + "routing" + ], + "author": "Maxim Zimaliev ", + "license": "Apache 2.0", + "bugs": { + "url": "https://github.com/slamdata/purescript-routing/issues" + }, + "homepage": "https://github.com/slamdata/purescript-routing", + "dependencies": { + "gulp": "^3.8.11", + "mandragora-bucket": "^0.1.12" + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..b63a548 --- /dev/null +++ b/public/index.html @@ -0,0 +1 @@ + diff --git a/src/Routing.purs b/src/Routing.purs new file mode 100644 index 0000000..87a728c --- /dev/null +++ b/src/Routing.purs @@ -0,0 +1,39 @@ +module Routing where + +import Control.Monad.Eff +import Data.Maybe +import Data.Either +import qualified Data.String.Regex as R + +import Routing.Parser +import Routing.Match + +foreign import hashChanged """ +function hashChanged(handler) { + return function() { + var currentHash = document.location.hash; + handler("")(currentHash)(); + window.addEventListener("hashchange", function(ev) { + handler(ev.oldURL)(ev.newURL)(); + }); + }; +} +""" :: forall e. (String -> String -> Eff e Unit) -> Eff e Unit + + +hashes :: forall e. (String -> String -> Eff e Unit) -> Eff e Unit +hashes cb = + hashChanged $ \old new -> do + cb (dropHash old) (dropHash new) + where dropHash h = R.replace (R.regex "^[^#]*#" R.noFlags) "" h + + +matches :: forall e a. Match a -> (Maybe a -> a -> Eff e Unit) -> Eff e Unit +matches routing cb = hashes $ \old new -> + let mr = matchHash routing + fst = either (const Nothing) Just $ mr old + in either (const $ pure unit) (cb fst) $ mr new + + +matchHash :: forall a. Match a -> String -> Either String a +matchHash matcher hash = runMatch matcher $ parse hash diff --git a/src/Routing/Match.purs b/src/Routing/Match.purs new file mode 100644 index 0000000..419ebd4 --- /dev/null +++ b/src/Routing/Match.purs @@ -0,0 +1,110 @@ +module Routing.Match where + +import Data.Either +import Data.Tuple +import Data.Maybe +import Data.List +import Control.Alt +import Control.Plus +import Control.Apply +import Control.Alternative +import Control.Monad.Error +import qualified Data.StrMap as M +import Global (readFloat, isNaN) +import Data.Semiring.Free +import Data.Foldable +import qualified Data.Array as A +import Data.Validation.Semiring + +import Routing.Parser +import Routing.Types +import Routing.Match.Class +import Routing.Match.Error + +newtype Match a = Match (Route -> V (Free MatchError) (Tuple Route a)) + +instance matchMatchClass :: MatchClass Match where + lit input = Match $ \route -> + case route of + Cons (Path i) rs | i == input -> + pure $ Tuple rs unit + Cons (Path _) rs -> + invalid $ free $ UnexpectedPath input + _ -> + invalid $ free ExpectedPathPart + num = Match $ \route -> + case route of + Cons (Path input) rs -> + let res = readFloat input in + if isNaN res then + invalid $ free ExpectedNumber + else + pure $ Tuple rs res + _ -> + invalid $ free ExpectedNumber + + bool = Match $ \route -> + case route of + Cons (Path input) rs | input == "true" -> + pure $ Tuple rs true + Cons (Path input) rs | input == "false" -> + pure $ Tuple rs false + _ -> + invalid $ free ExpectedBoolean + + str = Match $ \route -> + case route of + Cons (Path input) rs -> + pure $ Tuple rs input + _ -> + invalid $ free ExpectedString + + param key = Match $ \route -> + case route of + Cons (Query map) rs -> + case M.lookup key map of + Nothing -> + invalid $ free $ KeyNotFound key + Just el -> + pure $ Tuple (Cons (Query <<< M.delete key $ map) rs) el + _ -> + invalid $ free ExpectedQuery + fail msg = Match \_ -> + invalid $ free $ Fail msg + +instance matchFunctor :: Functor Match where + (<$>) fn (Match r2e) = Match $ \r -> + runV invalid (\(Tuple rs a) -> pure $ Tuple rs (fn a)) $ r2e r + +instance matchAlt :: Alt Match where + (<|>) (Match r2e1) (Match r2e2) = Match $ \r -> do + (r2e1 r) <|> (r2e2 r) + +instance matchPlus :: Plus Match where + empty = Match $ const $ invalid one + +instance matchAlternative :: Alternative Match + +instance matchApply :: Apply Match where + (<*>) (Match r2a2b) (Match r2a) = + Match $ (\r -> runV (processFnErr r) processFnRes (r2a2b r)) + where processFnErr r err = + invalid $ err * runV id (const one) (r2a r) + processFnRes (Tuple rs a2b) = + runV invalid (\(Tuple rss a) -> pure $ Tuple rss (a2b a)) (r2a rs) + +instance matchApplicative :: Applicative Match where + pure a = Match \r -> pure $ Tuple r a + +-- It groups `Free MatchError` -> [[MatchError]] -map with showMatchError -> +-- [[String]] -fold with semicolon-> [String] -fold with newline-> String +runMatch :: forall a. Match a -> Route -> Either String a +runMatch (Match fn) route = + runV foldErrors (Right <<< snd) $ fn route + where foldErrors errs = Left $ + foldl (\b a -> a <> "\n" <> b) "" do + es <- A.reverse <$> runFree errs + pure $ foldl (\b a -> a <> ";" <> b) "" $ showMatchError <$> es + + + diff --git a/src/Routing/Match/Class.purs b/src/Routing/Match/Class.purs new file mode 100644 index 0000000..e8cea9a --- /dev/null +++ b/src/Routing/Match/Class.purs @@ -0,0 +1,11 @@ +module Routing.Match.Class where + +import Control.Alternative + +class (Alternative f) <= MatchClass f where + lit :: String -> f Unit + str :: f String + param :: String -> f String + num :: f Number + bool :: f Boolean + fail :: forall a. String -> f a diff --git a/src/Routing/Match/Error.purs b/src/Routing/Match/Error.purs new file mode 100644 index 0000000..766c325 --- /dev/null +++ b/src/Routing/Match/Error.purs @@ -0,0 +1,31 @@ +module Routing.Match.Error where + +data MatchError + -- expected other path part + = UnexpectedPath String + -- expected "true" or "false" + | ExpectedBoolean + -- expected numeric literal + | ExpectedNumber + -- expected string literal (found query probably or eol) + | ExpectedString + -- expected query found path part or eol + | ExpectedQuery + -- expected path part found query or eol + | ExpectedPathPart + -- there is no such key in query + | KeyNotFound String + -- custom fail + | Fail String + +showMatchError :: MatchError -> String +showMatchError err = + case err of + UnexpectedPath str -> "expected path part: " <> str + KeyNotFound str -> "key: " <> str <> " has not found in query part" + ExpectedQuery -> "expected query - found path" + ExpectedNumber -> "expected number" + ExpectedBoolean -> "expected boolean" + ExpectedString -> "expected string var" + ExpectedPathPart -> "expected path part, found query" + Fail str -> "match error: " <> str diff --git a/src/Routing/Parser.purs b/src/Routing/Parser.purs new file mode 100644 index 0000000..b3b8d4a --- /dev/null +++ b/src/Routing/Parser.purs @@ -0,0 +1,36 @@ +module Routing.Parser ( + parse + ) where + +import Control.MonadPlus +import Data.Maybe +import Data.Tuple +import Data.List +import Data.Traversable (traverse) +import qualified Data.StrMap as M +import qualified Data.String as S +import qualified Data.Array as A + +import Routing.Types + + +tryQuery :: RoutePart -> RoutePart +tryQuery source@(Path string) = fromMaybe source $ do + guard $ S.take 1 string == "?" + let parts = S.split "&" $ S.drop 1 string + Query <$> M.fromList <$> traverse part2tuple parts + where part2tuple :: String -> Maybe (Tuple String String) + part2tuple input = do + let keyVal = S.split "=" input + guard $ A.length keyVal <= 2 + Tuple <$> (A.head keyVal) <*> (keyVal A.!! 1) +tryQuery q = q + +foreign import decodeURIComponent :: String -> String + +parse :: String -> Route +parse hash = tryQuery <$> + Path <$> + decodeURIComponent <$> + fromArray (S.split "/" hash) + diff --git a/src/Routing/Setter.purs b/src/Routing/Setter.purs new file mode 100644 index 0000000..f9c109c --- /dev/null +++ b/src/Routing/Setter.purs @@ -0,0 +1,20 @@ +module Routing.Setter where + +import Control.Monad.Eff + +foreign import setHash """ +function setHash(hash) { + return function() { + document.location.hash = hash; + }; +} +""" :: forall e. String -> Eff e Unit + + +-- | Class of types that can be converted to hashes +class RouteState a where + toHash :: a -> String + +-- | wrapper over `setHash` that uses `RouteState` +setRouteState :: forall r e. (RouteState r) => r -> Eff e Unit +setRouteState r = setHash $ toHash r diff --git a/src/Routing/Types.purs b/src/Routing/Types.purs new file mode 100644 index 0000000..917aa16 --- /dev/null +++ b/src/Routing/Types.purs @@ -0,0 +1,7 @@ +module Routing.Types where + +import qualified Data.StrMap as M +import Data.List + +data RoutePart = Path String | Query (M.StrMap String) +type Route = List RoutePart diff --git a/test/Main.purs b/test/Main.purs new file mode 100644 index 0000000..f5518d1 --- /dev/null +++ b/test/Main.purs @@ -0,0 +1,27 @@ +module Main where + +import Control.Monad.Eff +import Debug.Trace +import Control.Alt +import Control.Apply +import Debug.Foreign + + +import Routing +import Routing.Match +import Routing.Match.Class + +data FooBar = Foo Number | Bar Boolean String + +routing :: Match FooBar +routing = + Foo <$> (lit "foo" *> num) + <|> + Bar <$> (lit "bar" *> bool) <*> (param "baz") + +main = do + fprint $ matchHash routing "food/asdf" + matches routing $ \old new -> void $ do + fprint old + fprint new +