├── .gitignore ├── LICENSE ├── README.md ├── elm.json ├── examples ├── Example.elm ├── README.md └── elm-package.json ├── package.json ├── src └── UrlParser.elm └── tests ├── .gitignore ├── Main.elm ├── Tests.elm └── elm-package.json /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Evan Czaplicki 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # URL Parser 2 | 3 | This library helps you turn URLs into nicely structured data. 4 | 5 | It is designed to be used with `elm-lang/navigation` to help folks create single-page applications (SPAs) where you manage browser navigation yourself. 6 | 7 | > **Note:** This library is meant to serve as a baseline for future URL parsers. For example, it does not handle query parameters and hashes right now. It is more to (1) get folks started using URL parsers and (2) help us gather data on exactly which scenarios people face. 8 | 9 | 10 | ## Examples 11 | 12 | Here is a simplified REPL session showing a parser in action: 13 | 14 | ```elm 15 | > import UrlParser exposing ((), s, int, string, parseHash) 16 | 17 | > parseHash (s "blog" int) { ... , hash = "#blog/42" } 18 | Just 42 19 | 20 | > parseHash (s "blog" int) { ... , hash = "#/blog/13" } 21 | Just 13 22 | 23 | > parseHash (s "blog" int) { ... , hash = "#/blog/hello" } 24 | Nothing 25 | 26 | > parseHash (s "search" string) { ... , hash = "#search/dogs" } 27 | Just "dogs" 28 | 29 | > parseHash (s "search" string) { ... , hash = "#/search/13" } 30 | Just "13" 31 | 32 | > parseHash (s "search" string) { ... , hash = "#/search" } 33 | Nothing 34 | ``` 35 | 36 | Normally you have to put many of these parsers to handle all possible pages though! The following parser works on URLs like `/blog/42` and `/search/badger`: 37 | 38 | ```elm 39 | import UrlParser exposing (Parser, (), s, int, string, map, oneOf, parseHash) 40 | 41 | type Route = Blog Int | Search String 42 | 43 | route : Parser (Route -> a) a 44 | route = 45 | oneOf 46 | [ map Blog (s "blog" int) 47 | , map Search (s "search" string) 48 | ] 49 | 50 | -- parseHash route { ... , hash = "#/blog/58" } == Just (Blog 58) 51 | -- parseHash route { ... , hash = "#/search/cat" } == Just (Search "cat") 52 | -- parseHash route { ... , hash = "#/search/31" } == Just (Search "31") 53 | -- parseHash route { ... , hash = "#/blog/cat" } == Nothing 54 | -- parseHash route { ... , hash = "#/blog" } == Nothing 55 | ``` 56 | 57 | Notice that we are turning URLs into nice [union types](https://guide.elm-lang.org/types/union_types.html), so we can use `case` expressions to work with them in a nice way. 58 | 59 | Check out the `examples/` directory of this repo to see this in use with `elm-lang/navigation`. 60 | 61 | 62 | ## Testing 63 | 64 | ``` 65 | npm install 66 | npm test 67 | ``` 68 | 69 | ## Background 70 | 71 | I first saw this general idea in Chris Done’s [formatting][] library. Based on that, Noah and I outlined the API you see in this library. Noah then found Rudi Grinberg’s [post][] about type safe routing in OCaml. It was exactly what we were going for. We had even used the names `s` and `()` in our draft API! In the end, we ended up using the “final encoding” of the EDSL that had been left as an exercise for the reader. Very fun to work through! 72 | 73 | [formatting]: http://chrisdone.com/posts/formatting 74 | [post]: http://rgrinberg.com/posts/primitive-type-safe-routing/ 75 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "elm-lang/url-parser", 4 | "summary": "Parse URLs, useful for navigation and \"routing\" in single-page apps (SPA)", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.0", 7 | "exposed-modules": [ 8 | "UrlParser" 9 | ], 10 | "elm-version": "0.19.0 <= v < 0.20.0", 11 | "dependencies": { 12 | "elm-lang/core": "6.0.0 <= v < 7.0.0" 13 | }, 14 | "test-dependencies": { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/Example.elm: -------------------------------------------------------------------------------- 1 | import Html exposing (Html, a, button, code, div, h1, li, text, ul) 2 | import Html.Attributes exposing (href) 3 | import Html.Events exposing (onClick) 4 | import Http 5 | import Navigation 6 | import UrlParser as Url exposing ((), (), s, int, stringParam, top) 7 | 8 | 9 | 10 | main = 11 | Navigation.program UrlChange 12 | { init = init 13 | , view = view 14 | , update = update 15 | , subscriptions = subscriptions 16 | } 17 | 18 | 19 | 20 | -- MODEL 21 | 22 | 23 | type alias Model = 24 | { history : List (Maybe Route) 25 | } 26 | 27 | 28 | init : Navigation.Location -> ( Model, Cmd Msg ) 29 | init location = 30 | ( Model [Url.parsePath route location] 31 | , Cmd.none 32 | ) 33 | 34 | 35 | 36 | -- URL PARSING 37 | 38 | 39 | type Route 40 | = Home 41 | | BlogList (Maybe String) 42 | | BlogPost Int 43 | 44 | 45 | route : Url.Parser (Route -> a) a 46 | route = 47 | Url.oneOf 48 | [ Url.map Home top 49 | , Url.map BlogList (s "blog" stringParam "search") 50 | , Url.map BlogPost (s "blog" int) 51 | ] 52 | 53 | 54 | 55 | -- UPDATE 56 | 57 | 58 | type Msg 59 | = NewUrl String 60 | | UrlChange Navigation.Location 61 | 62 | 63 | update : Msg -> Model -> ( Model, Cmd Msg ) 64 | update msg model = 65 | case msg of 66 | NewUrl url -> 67 | ( model 68 | , Navigation.newUrl url 69 | ) 70 | 71 | UrlChange location -> 72 | ( { model | history = Url.parsePath route location :: model.history } 73 | , Cmd.none 74 | ) 75 | 76 | 77 | 78 | -- SUBSCRIPTIONS 79 | 80 | 81 | subscriptions : Model -> Sub Msg 82 | subscriptions model = 83 | Sub.none 84 | 85 | 86 | 87 | -- VIEW 88 | 89 | 90 | view : Model -> Html Msg 91 | view model = 92 | div [] 93 | [ h1 [] [ text "Links" ] 94 | , ul [] (List.map viewLink [ "/", "/blog/", "/blog/42", "/blog/37", "/blog/?search=cats" ]) 95 | , h1 [] [ text "History" ] 96 | , ul [] (List.map viewRoute model.history) 97 | ] 98 | 99 | 100 | viewLink : String -> Html Msg 101 | viewLink url = 102 | li [] [ button [ onClick (NewUrl url) ] [ text url ] ] 103 | 104 | 105 | viewRoute : Maybe Route -> Html msg 106 | viewRoute maybeRoute = 107 | case maybeRoute of 108 | Nothing -> 109 | li [] [ text "Invalid URL"] 110 | 111 | Just route -> 112 | li [] [ code [] [ text (routeToString route) ] ] 113 | 114 | 115 | routeToString : Route -> String 116 | routeToString route = 117 | case route of 118 | Home -> 119 | "home" 120 | 121 | BlogList Nothing -> 122 | "list all blog posts" 123 | 124 | BlogList (Just search) -> 125 | "search for " ++ Http.encodeUri search 126 | 127 | BlogPost id -> 128 | "show blog " ++ toString id -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Run the Examples 2 | 3 | To run the examples in this folder, follow the following steps: 4 | 5 | ```bash 6 | git clone https://github.com/evancz/url-parser.git 7 | cd url-parser 8 | cd examples 9 | elm-reactor 10 | ``` 11 | 12 | This will navigate into the `examples/` directory and start `elm-reactor`. From here, go to [http://localhost:8000](http://localhost:8000) and start clicking on `.elm` files to see them in action. -------------------------------------------------------------------------------- /examples/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Examples of using evancz/url-parser", 4 | "repository": "http://github.com/user/project.git", 5 | "license": "BSD-3-Clause", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 12 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 13 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 14 | "elm-lang/navigation": "2.0.0 <= v < 3.0.0", 15 | "evancz/url-parser": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.18.0 <= v < 0.19.0" 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "elm-test" 4 | }, 5 | "dependencies": { 6 | "elm-test": "^0.18.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/UrlParser.elm: -------------------------------------------------------------------------------- 1 | module UrlParser exposing 2 | ( Parser, string, int, s 3 | , (), map, oneOf, top, custom 4 | , QueryParser, (), stringParam, intParam, customParam 5 | , parsePath, parseHash 6 | ) 7 | 8 | {-| 9 | 10 | # Primitives 11 | @docs Parser, string, int, s 12 | 13 | # Path Parses 14 | @docs (), map, oneOf, top, custom 15 | 16 | # Query Parameter Parsers 17 | @docs QueryParser, (), stringParam, intParam, customParam 18 | 19 | # Run a Parser 20 | @docs parsePath, parseHash 21 | 22 | -} 23 | 24 | import Dict exposing (Dict) 25 | import Http 26 | import Navigation 27 | 28 | 29 | 30 | -- PARSERS 31 | 32 | 33 | {-| Turn URLs like `/blog/42/cat-herding-techniques` into nice Elm data. 34 | -} 35 | type Parser a b = 36 | Parser (State a -> List (State b)) 37 | 38 | 39 | type alias State value = 40 | { visited : List String 41 | , unvisited : List String 42 | , params : Dict String String 43 | , value : value 44 | } 45 | 46 | 47 | 48 | -- PARSE SEGMENTS 49 | 50 | 51 | {-| Parse a segment of the path as a `String`. 52 | 53 | parsePath string location 54 | -- /alice/ ==> Just "alice" 55 | -- /bob ==> Just "bob" 56 | -- /42/ ==> Just "42" 57 | -} 58 | string : Parser (String -> a) a 59 | string = 60 | custom "STRING" Ok 61 | 62 | 63 | {-| Parse a segment of the path as an `Int`. 64 | 65 | parsePath int location 66 | -- /alice/ ==> Nothing 67 | -- /bob ==> Nothing 68 | -- /42/ ==> Just 42 69 | -} 70 | int : Parser (Int -> a) a 71 | int = 72 | custom "NUMBER" String.toInt 73 | 74 | 75 | {-| Parse a segment of the path if it matches a given string. 76 | 77 | s "blog" -- can parse /blog/ 78 | -- but not /glob/ or /42/ or anything else 79 | -} 80 | s : String -> Parser a a 81 | s str = 82 | Parser <| \{ visited, unvisited, params, value } -> 83 | case unvisited of 84 | [] -> 85 | [] 86 | 87 | next :: rest -> 88 | if next == str then 89 | [ State (next :: visited) rest params value ] 90 | 91 | else 92 | [] 93 | 94 | 95 | {-| Create a custom path segment parser. Here is how it is used to define the 96 | `int` and `string` parsers: 97 | 98 | int = 99 | custom "NUMBER" String.toInt 100 | 101 | string = 102 | custom "STRING" Ok 103 | 104 | You can use it to define something like “only CSS files” like this: 105 | 106 | css : Parser (String -> a) a 107 | css = 108 | custom "CSS_FILE" <| \segment -> 109 | if String.endsWith ".css" segment then 110 | Ok segment 111 | else 112 | Err "Does not end with .css" 113 | -} 114 | custom : String -> (String -> Result String a) -> Parser (a -> b) b 115 | custom tipe stringToSomething = 116 | Parser <| \{ visited, unvisited, params, value } -> 117 | case unvisited of 118 | [] -> 119 | [] 120 | 121 | next :: rest -> 122 | case stringToSomething next of 123 | Ok nextValue -> 124 | [ State (next :: visited) rest params (value nextValue) ] 125 | 126 | Err msg -> 127 | [] 128 | 129 | 130 | 131 | -- COMBINING PARSERS 132 | 133 | 134 | {-| Parse a path with multiple segments. 135 | 136 | parsePath (s "blog" int) location 137 | -- /blog/35/ ==> Just 35 138 | -- /blog/42 ==> Just 42 139 | -- /blog/ ==> Nothing 140 | -- /42/ ==> Nothing 141 | 142 | parsePath (s "search" string) location 143 | -- /search/cats/ ==> Just "cats" 144 | -- /search/frog ==> Just "frog" 145 | -- /search/ ==> Nothing 146 | -- /cats/ ==> Nothing 147 | -} 148 | () : Parser a b -> Parser b c -> Parser a c 149 | () (Parser parseBefore) (Parser parseAfter) = 150 | Parser <| \state -> 151 | List.concatMap parseAfter (parseBefore state) 152 | 153 | 154 | infixr 7 155 | 156 | 157 | {-| Transform a path parser. 158 | 159 | type alias Comment = { author : String, id : Int } 160 | 161 | rawComment : Parser (String -> Int -> a) a 162 | rawComment = 163 | s "user" string s "comments" int 164 | 165 | comment : Parser (Comment -> a) a 166 | comment = 167 | map Comment rawComment 168 | 169 | parsePath comment location 170 | -- /user/bob/comments/42 ==> Just { author = "bob", id = 42 } 171 | -- /user/tom/comments/35 ==> Just { author = "tom", id = 35 } 172 | -- /user/sam/ ==> Nothing 173 | -} 174 | map : a -> Parser a b -> Parser (b -> c) c 175 | map subValue (Parser parse) = 176 | Parser <| \{ visited, unvisited, params, value } -> 177 | List.map (mapHelp value) <| parse <| 178 | { visited = visited 179 | , unvisited = unvisited 180 | , params = params 181 | , value = subValue 182 | } 183 | 184 | 185 | mapHelp : (a -> b) -> State a -> State b 186 | mapHelp func {visited, unvisited, params, value} = 187 | { visited = visited 188 | , unvisited = unvisited 189 | , params = params 190 | , value = func value 191 | } 192 | 193 | 194 | {-| Try a bunch of different path parsers. 195 | 196 | type Route 197 | = Search String 198 | | Blog Int 199 | | User String 200 | | Comment String Int 201 | 202 | route : Parser (Route -> a) a 203 | route = 204 | oneOf 205 | [ map Search (s "search" string) 206 | , map Blog (s "blog" int) 207 | , map User (s "user" string) 208 | , map Comment (s "user" string s "comments" int) 209 | ] 210 | 211 | parsePath route location 212 | -- /search/cats ==> Just (Search "cats") 213 | -- /search/ ==> Nothing 214 | 215 | -- /blog/42 ==> Just (Blog 42) 216 | -- /blog/cats ==> Nothing 217 | 218 | -- /user/sam/ ==> Just (User "sam") 219 | -- /user/bob/comments/42 ==> Just (Comment "bob" 42) 220 | -- /user/tom/comments/35 ==> Just (Comment "tom" 35) 221 | -- /user/ ==> Nothing 222 | 223 | -} 224 | oneOf : List (Parser a b) -> Parser a b 225 | oneOf parsers = 226 | Parser <| \state -> 227 | List.concatMap (\(Parser parser) -> parser state) parsers 228 | 229 | 230 | {-| A parser that does not consume any path segments. 231 | 232 | type BlogRoute = Overview | Post Int 233 | 234 | blogRoute : Parser (BlogRoute -> a) a 235 | blogRoute = 236 | oneOf 237 | [ map Overview top 238 | , map Post (s "post" int) 239 | ] 240 | 241 | parsePath (s "blog" blogRoute) location 242 | -- /blog/ ==> Just Overview 243 | -- /blog/post/42 ==> Just (Post 42) 244 | -} 245 | top : Parser a a 246 | top = 247 | Parser <| \state -> [state] 248 | 249 | 250 | 251 | -- QUERY PARAMETERS 252 | 253 | 254 | {-| Turn query parameters like `?name=tom&age=42` into nice Elm data. 255 | -} 256 | type QueryParser a b = 257 | QueryParser (State a -> List (State b)) 258 | 259 | 260 | {-| Parse some query parameters. 261 | 262 | type Route = BlogList (Maybe String) | BlogPost Int 263 | 264 | route : Parser (Route -> a) a 265 | route = 266 | oneOf 267 | [ map BlogList (s "blog" stringParam "search") 268 | , map BlogPost (s "blog" int) 269 | ] 270 | 271 | parsePath route location 272 | -- /blog/ ==> Just (BlogList Nothing) 273 | -- /blog/?search=cats ==> Just (BlogList (Just "cats")) 274 | -- /blog/42 ==> Just (BlogPost 42) 275 | -} 276 | () : Parser a b -> QueryParser b c -> Parser a c 277 | () (Parser parser) (QueryParser queryParser) = 278 | Parser <| \state -> 279 | List.concatMap queryParser (parser state) 280 | 281 | 282 | infixl 8 283 | 284 | 285 | {-| Parse a query parameter as a `String`. 286 | 287 | parsePath (s "blog" stringParam "search") location 288 | -- /blog/ ==> Just (Overview Nothing) 289 | -- /blog/?search=cats ==> Just (Overview (Just "cats")) 290 | -} 291 | stringParam : String -> QueryParser (Maybe String -> a) a 292 | stringParam name = 293 | customParam name identity 294 | 295 | 296 | {-| Parse a query parameter as an `Int`. Maybe you want to show paginated 297 | search results. You could have a `start` query parameter to say which result 298 | should appear first. 299 | 300 | parsePath (s "results" intParam "start") location 301 | -- /results ==> Just Nothing 302 | -- /results?start=10 ==> Just (Just 10) 303 | -} 304 | intParam : String -> QueryParser (Maybe Int -> a) a 305 | intParam name = 306 | customParam name intParamHelp 307 | 308 | 309 | intParamHelp : Maybe String -> Maybe Int 310 | intParamHelp maybeValue = 311 | case maybeValue of 312 | Nothing -> 313 | Nothing 314 | 315 | Just value -> 316 | Result.toMaybe (String.toInt value) 317 | 318 | 319 | {-| Create a custom query parser. You could create parsers like these: 320 | 321 | jsonParam : String -> Decoder a -> QueryParser (Maybe a -> b) b 322 | enumParam : String -> Dict String a -> QueryParser (Maybe a -> b) b 323 | 324 | It may be worthwhile to have these in this library directly. If you need 325 | either one in practice, please open an issue [here][] describing your exact 326 | scenario. We can use that data to decide if they should be added. 327 | 328 | [here]: https://github.com/evancz/url-parser/issues 329 | -} 330 | customParam : String -> (Maybe String -> a) -> QueryParser (a -> b) b 331 | customParam key func = 332 | QueryParser <| \{ visited, unvisited, params, value } -> 333 | [ State visited unvisited params (value (func (Dict.get key params))) ] 334 | 335 | 336 | 337 | -- RUN A PARSER 338 | 339 | 340 | {-| Parse based on `location.pathname` and `location.search`. This parser 341 | ignores the hash entirely. 342 | -} 343 | parsePath : Parser (a -> a) a -> Navigation.Location -> Maybe a 344 | parsePath parser location = 345 | parse parser location.pathname (parseParams location.search) 346 | 347 | 348 | {-| Parse based on `location.hash` and `location.search`. This parser 349 | ignores the normal path entirely. 350 | -} 351 | parseHash : Parser (a -> a) a -> Navigation.Location -> Maybe a 352 | parseHash parser location = 353 | parse parser (String.dropLeft 1 location.hash) (parseParams location.search) 354 | 355 | 356 | 357 | -- PARSER HELPERS 358 | 359 | 360 | parse : Parser (a -> a) a -> String -> Dict String String -> Maybe a 361 | parse (Parser parser) url params = 362 | parseHelp <| parser <| 363 | { visited = [] 364 | , unvisited = splitUrl url 365 | , params = params 366 | , value = identity 367 | } 368 | 369 | 370 | parseHelp : List (State a) -> Maybe a 371 | parseHelp states = 372 | case states of 373 | [] -> 374 | Nothing 375 | 376 | state :: rest -> 377 | case state.unvisited of 378 | [] -> 379 | Just state.value 380 | 381 | [""] -> 382 | Just state.value 383 | 384 | _ -> 385 | parseHelp rest 386 | 387 | 388 | splitUrl : String -> List String 389 | splitUrl url = 390 | case String.split "/" url of 391 | "" :: segments -> 392 | segments 393 | 394 | segments -> 395 | segments 396 | 397 | 398 | parseParams : String -> Dict String String 399 | parseParams queryString = 400 | queryString 401 | |> String.dropLeft 1 402 | |> String.split "&" 403 | |> List.filterMap toKeyValuePair 404 | |> Dict.fromList 405 | 406 | 407 | toKeyValuePair : String -> Maybe (String, String) 408 | toKeyValuePair segment = 409 | case String.split "=" segment of 410 | [key, value] -> 411 | Maybe.map2 (,) (Http.decodeUri key) (Http.decodeUri value) 412 | 413 | _ -> 414 | Nothing 415 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/ 2 | -------------------------------------------------------------------------------- /tests/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (..) 2 | 3 | import Tests 4 | import Test.Runner.Node exposing (run, TestProgram) 5 | import Json.Encode exposing (Value) 6 | 7 | 8 | main : TestProgram 9 | main = 10 | run emit Tests.all 11 | 12 | 13 | port emit : ( String, Value ) -> Cmd msg 14 | -------------------------------------------------------------------------------- /tests/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing (..) 2 | 3 | import UrlParser exposing (..) 4 | import Navigation exposing (Location) 5 | import Test exposing (..) 6 | import Expect 7 | 8 | 9 | 10 | -- TESTS 11 | 12 | 13 | all : Test 14 | all = 15 | describe "UrlParser" 16 | [ describe "Basic Parsing" testParsing 17 | ] 18 | 19 | 20 | testParsing = 21 | [ parserTest "Home" "" HomeRoute 22 | , parserTest "About" "about" AboutRoute 23 | , parserTest "Token" "token/abc" (TokenRoute "abc") 24 | , parserTest "Users" "users" (UsersRoutes UsersRoute) 25 | , parserTest "User" "users/2" (UsersRoutes (UserRoute 2)) 26 | , parserTest "Edit" "users/2/edit" (UsersRoutes (UserEditRoute 2)) 27 | ] 28 | 29 | 30 | parserTest name path expectedRoute = 31 | describe name 32 | [ test (name ++ " in path") <| 33 | \() -> 34 | Expect.equal 35 | (Just expectedRoute) 36 | (parsePath routeParser { newLocation | pathname = "/" ++ path }) 37 | , test (name ++ " in hash") <| 38 | \() -> 39 | Expect.equal 40 | (Just expectedRoute) 41 | (parseHash routeParser { newLocation | hash = "#/" ++ path }) 42 | , test (name ++ "in hash without leading slash") <| 43 | \() -> 44 | Expect.equal 45 | (Just expectedRoute) 46 | (parseHash routeParser { newLocation | hash = "#" ++ path }) 47 | ] 48 | 49 | 50 | 51 | -- ROUTES 52 | 53 | 54 | type alias UserId = 55 | Int 56 | 57 | 58 | type UserRoute 59 | = UsersRoute 60 | | UserRoute UserId 61 | | UserEditRoute UserId 62 | 63 | 64 | type MainRoute 65 | = HomeRoute 66 | | AboutRoute 67 | | TokenRoute String 68 | | UsersRoutes UserRoute 69 | | NotFoundRoute 70 | 71 | 72 | 73 | -- PARSERS 74 | 75 | 76 | routeParser = 77 | oneOf mainMatchers 78 | 79 | 80 | usersMatchers = 81 | [ map UserEditRoute (int s "edit") 82 | , map UserRoute (int) 83 | , map UsersRoute top 84 | ] 85 | 86 | 87 | mainMatchers = 88 | [ map HomeRoute top 89 | , map AboutRoute (s "about") 90 | , map TokenRoute (s "token" string) 91 | , map UsersRoutes (s "users" (oneOf usersMatchers)) 92 | ] 93 | 94 | 95 | 96 | -- DUMMY LOCATION 97 | 98 | 99 | newLocation : Location 100 | newLocation = 101 | { hash = "" 102 | , host = "example.com" 103 | , hostname = "example.com" 104 | , href = "" 105 | , origin = "" 106 | , password = "" 107 | , pathname = "" 108 | , port_ = "" 109 | , protocol = "http" 110 | , search = "" 111 | , username = "" 112 | } 113 | -------------------------------------------------------------------------------- /tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Sample Elm Test", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD-3-Clause", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 13 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 14 | "elm-community/elm-test": "3.0.0 <= v < 4.0.0", 15 | "elm-lang/navigation": "2.0.0 <= v < 3.0.0", 16 | "rtfeldman/node-test-runner": "3.0.0 <= v < 4.0.0" 17 | }, 18 | "elm-version": "0.18.0 <= v < 0.19.0" 19 | } 20 | --------------------------------------------------------------------------------