diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for dormouse
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Phil Curzon (c) 2020-2021
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Phil Curzon nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+# Dormouse-Uri
+
+Dormouse-Uri provides type safe handling of `Uri`s and `Url`s.
+
+`Uri` sytax is well defined according to [RFC 3986](https://tools.ietf.org/html/rfc3986), Dormouse-Uri parses and encodes `Uri`s according to the syntax defined in this document.
+
+We define `Url` as an absolute URI associated with web resources, the current version of Dormouse-Uri restricts `Url`s to the `http` and `https` schemes.
+
+Dormouse-Uri has the following features:
+
+ - The `Uri`/`Url` data types use `Data.Text` internally, this allows you to freely include percent-decoded characters which will be properly rendered when the `Url`/`Uri` is encoded.
+ - Quasiquoters to allow safe construction of `Uri`/`Url`s from string literals.
+ - `DataKinds` allow `Url`s to be restricted to the `http` or `https` schemes are the type level.
+ - A UrlBuilder syntax to allow type-safe construction/concatenation of `Url`s from their components, e.g. path and query parameters.
+
+ ## Constructing Uris
+
+ ```haskell
+{-# LANGUAGE QuasiQuotes #-}
+
+import Dormouse.Uri
+import Dormouse.Uri.QQ
+
+telUri :: Uri
+telUri = [uri|tel:+1-816-555-1212|]
+
+mailtoUri :: Uri
+mailtoUri = [uri|mailto:John.Doe@example.com|]
+
+httpUri :: Uri
+httpUri = [uri|http://haskell.org|]
+ ```
+
+ ## Constructing Urls
+
+You can construct Urls using the helper QuasiQuoters:
+
+```haskell
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+import Dormouse.Url
+import Dormouse.Url.QQ
+
+githubHttpsUrl :: Url "https"
+githubHttpsUrl = [https|https://github.com|]
+
+githubHttpUrl :: Url "http"
+githubHttpUrl = [http|http://github.com|]
+
+githubAnyUrl :: AnyUrl
+githubAnyUrl = [url|http://github.com|]
+```
+
+You can use the Url Builder syntax to modify an existing Url safely to include paths, adding the import:
+
+```haskell
+import Dormouse.Url.Builder
+```
+
+To allow:
+
+```haskell
+dormouseHttpsUrl :: Url "https"
+dormouseHttpsUrl = githubHttpsUrl </> "TheInnerLight" </> "dormouse"
+```
+
+The Url will be constructed safely so that any characters that wouldn't normally be allowed in a Url path are percent-encoded before the url is resolved by Dormouse.
+
+You can also handle query parameters using similar syntax:
+
+```haskell
+searchUrl :: Url "https"
+searchUrl = [https|https://google.com|] </> "search" ? "q" =: ("haskell" :: String)
+```
diff --git a/dormouse-uri.cabal b/dormouse-uri.cabal
new file mode 100644
--- /dev/null
+++ b/dormouse-uri.cabal
@@ -0,0 +1,121 @@
+cabal-version: 1.18
+
+-- This file has been generated from package.yaml by hpack version 0.33.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 7c88a7285ad48f5d51d430c58efcedff76ebd29951e6a7cb06ab16b8b38b1909
+
+name:           dormouse-uri
+version:        0.1.0.0
+synopsis:       Library for type-safe representations of Uri/Urls
+description:    Dormouse-Uri provides type safe handling of `Uri`s and `Url`s.
+                .
+                `Uri` sytax is well defined according to [RFC 3986](https://tools.ietf.org/html/rfc3986), Dormouse-Uri parses and encodes `Uri`s according to the syntax defined in this document.
+                .
+                We define `Url` as an absolute URI associated with web resources, the current version of Dormouse-Uri restricts `Url`s to the `http` and `https` schemes.
+                .
+                Dormouse-Uri has the following features:
+                .
+                  - The `Uri` and `Url` data types use `Data.Text` internally, this allows you to freely include percent-decoded characters which will be properly rendered when the `Url`/`Uri` is encoded.
+                  - Quasiquoters to allow safe construction of `Uri`/`Url`s from string literals.
+                  - `DataKinds` allow `Url`s to be restricted to the `http` or `https` schemes are the type level.
+                  - A UrlBuilder syntax to allow type-safe construction/concatenation of `Url`s from their components, e.g. path and query parameters.
+                .
+                Please see https://dormouse.io for full documentation.
+category:       Web
+homepage:       https://dormouse.io/uri.html
+bug-reports:    https://github.com/theinnerlight/dormouse/issues
+author:         Phil Curzon
+maintainer:     phil@novelfs.org
+copyright:      2020-2021 Phil Curzon
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+extra-doc-files:
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/theinnerlight/dormouse
+
+library
+  exposed-modules:
+      Dormouse.Uri.RFC3986
+      Dormouse.Uri.Encode
+      Dormouse.Uri.Exception
+      Dormouse.Url.Builder
+      Dormouse.Url.Exception
+      Dormouse.Uri.QQ
+      Dormouse.Url.QQ
+      Dormouse.Uri
+      Dormouse.Url
+  other-modules:
+      Dormouse.Uri.Parser
+      Dormouse.Uri.Types
+      Dormouse.Url.Class
+      Dormouse.Url.Types
+      Paths_dormouse_uri
+  hs-source-dirs:
+      src
+  default-extensions: OverloadedStrings MultiParamTypeClasses ScopedTypeVariables FlexibleContexts
+  ghc-options: -Wall
+  build-depends:
+      attoparsec >=0.13.2.4 && <0.14
+    , base >=4.7 && <5
+    , bytestring >=0.10.8 && <0.11.0
+    , case-insensitive >=1.2.1.0 && <2.0.0
+    , containers >=0.6.2.1 && <0.7
+    , http-types >=0.12.3 && <0.13
+    , safe-exceptions >=0.1.7 && <0.2.0
+    , template-haskell >=2.15.0 && <3.0.0
+    , text >=1.2.3 && <2.0.0
+  default-language: Haskell2010
+
+test-suite dormouse-uri-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Dormouse.Uri
+      Dormouse.Uri.Encode
+      Dormouse.Uri.Exception
+      Dormouse.Uri.Parser
+      Dormouse.Uri.QQ
+      Dormouse.Uri.RFC3986
+      Dormouse.Uri.Types
+      Dormouse.Url
+      Dormouse.Url.Builder
+      Dormouse.Url.Class
+      Dormouse.Url.Exception
+      Dormouse.Url.QQ
+      Dormouse.Url.Types
+      Dormouse.Generators.UriComponents
+      Dormouse.Uri.ParserSpec
+      Dormouse.Uri.QQSpec
+      Dormouse.Uri.QuerySpec
+      Paths_dormouse_uri
+  hs-source-dirs:
+      src
+      test
+  default-extensions: OverloadedStrings MultiParamTypeClasses ScopedTypeVariables FlexibleContexts
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
+  build-depends:
+      attoparsec >=0.13.2.4 && <0.14
+    , base >=4.7 && <5
+    , bytestring >=0.10.8 && <0.11.0
+    , case-insensitive >=1.2.1.0 && <2.0.0
+    , containers >=0.6.2.1 && <0.7
+    , hedgehog >=1.0.1 && <2
+    , hspec >=2.0.0 && <3
+    , hspec-discover >=2.0.0 && <3
+    , hspec-hedgehog >=0.0.1.2 && <0.1
+    , http-types >=0.12.3 && <0.13
+    , safe-exceptions >=0.1.7 && <0.2.0
+    , scientific >=0.3.6.2 && <0.4
+    , template-haskell >=2.15.0 && <3.0.0
+    , text >=1.2.3 && <2.0.0
+    , vector >=0.12.0.3 && <0.13
+  default-language: Haskell2010
diff --git a/src/Dormouse/Uri.hs b/src/Dormouse/Uri.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Uri.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE KindSignatures #-}
+
+module Dormouse.Uri
+  ( module Dormouse.Uri.Types
+  , parseUri
+  ) where
+
+import Control.Exception.Safe (MonadThrow, throw)
+import qualified Data.ByteString as SB
+import Data.Attoparsec.ByteString.Char8 (parseOnly)
+import qualified Data.Text as T
+import Dormouse.Uri.Exception (UriException(..))
+import Dormouse.Uri.Parser
+import Dormouse.Uri.Types
+
+-- | Parse an ascii 'ByteString' as an absolute uri, throwing a 'UriException' in @m@ if this fails
+parseUri :: MonadThrow m => SB.ByteString -> m Uri
+parseUri bs = either (throw . UriException . T.pack) return $ parseOnly pUri bs
diff --git a/src/Dormouse/Uri/Encode.hs b/src/Dormouse/Uri/Encode.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Uri/Encode.hs
@@ -0,0 +1,44 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Dormouse.Uri.Encode
+  ( encodeQuery
+  , encodePath
+  , encodeUnless
+  ) where
+
+import Data.Char (chr)
+import Data.Word (Word8)
+import qualified Data.ByteString as B
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as E
+import Dormouse.Uri.Types
+import Dormouse.Uri.RFC3986
+
+-- | Percent encode a word8 as an ascii 'Bytestring'
+percentEncode :: Word8 -> B.ByteString
+percentEncode w = 
+  let h = w `div` 16
+      l = w `mod` 16 in
+  B.pack [37, hex h, hex l]
+  where 
+    hex x
+     | x < 10    = 48+x
+     | otherwise = 55+x
+
+-- | Percent encode all chars in the supplied text except for those which satsify the supplied predicate
+encodeUnless :: (Char -> Bool)  -> T.Text -> B.ByteString
+encodeUnless isAllowedChar = B.concatMap pEncodeQuery . E.encodeUtf8
+  where
+    pEncodeQuery :: Word8 -> B.ByteString
+    pEncodeQuery c
+      | isAllowedChar (chr $ fromEnum c) = B.singleton c
+      | otherwise                         = percentEncode c
+
+-- | Generate an ascii `Bytestring` from a supplied Query by percent encoding all of the invalid octets
+encodeQuery :: Query -> B.ByteString
+encodeQuery = B.append "?" . encodeUnless isQueryChar . unQuery
+
+-- | Generate an ascii `Bytestring` from a supplied Path by percent encoding all of the invalid octets
+encodePath :: Path 'Absolute -> B.ByteString
+encodePath =  B.append "/" . B.intercalate "/" . fmap (encodeUnless isPathChar . unPathSegment) . unPath
diff --git a/src/Dormouse/Uri/Exception.hs b/src/Dormouse/Uri/Exception.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Uri/Exception.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE ExistentialQuantification #-}
+
+module Dormouse.Uri.Exception
+  ( UriException(..)
+  ) where
+
+import Control.Exception.Safe (Exception(..))
+import qualified Data.Text as T
+
+-- | 'UriException' is used to indicate an error parsing a URI
+newtype UriException = UriException  { uriExceptionMessage :: T.Text }
+
+instance Show UriException where
+  show UriException { uriExceptionMessage = msg } = "Failed to parse uri: " <> T.unpack msg
+
+instance Exception UriException
diff --git a/src/Dormouse/Uri/Parser.hs b/src/Dormouse/Uri/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Uri/Parser.hs
@@ -0,0 +1,230 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DisambiguateRecordFields #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Dormouse.Uri.Parser
+  ( pUri
+  , pAbsoluteUri
+  , pRelativeUri
+  , pScheme
+  , pUsername
+  , pPassword
+  , pUserInfo
+  , pIPv4
+  , pRegName
+  , pHost
+  , pPort
+  , pAuthority
+  , pPathAbsAuth
+  , pPathAbsNoAuth
+  , pPathRel
+  , pQuery
+  , pFragment
+  ) where
+
+import Control.Applicative ((<|>))
+import Data.Attoparsec.ByteString.Char8 as A
+import Data.Char as C
+import Data.Bits (Bits, shiftL, (.|.))
+import Data.Maybe (isJust)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as TE
+import Dormouse.Uri.Types
+import Dormouse.Uri.RFC3986
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as B8
+
+repack :: String -> T.Text
+repack = TE.decodeUtf8 . B8.pack
+
+pMaybe :: Parser a -> Parser (Maybe a)
+pMaybe p = option Nothing (Just <$> p)
+
+pAsciiAlpha :: Parser Char
+pAsciiAlpha = satisfy isAsciiAlpha
+
+pAsciiAlphaNumeric :: Parser Char
+pAsciiAlphaNumeric = satisfy isAsciiAlphaNumeric
+
+pSubDelim :: Parser Char
+pSubDelim = satisfy isSubDelim
+
+pUnreserved :: Parser Char
+pUnreserved = satisfy isUnreserved
+
+pSizedHexadecimal :: (Integral a, Bits a) => Int -> Parser a
+pSizedHexadecimal n = do
+    bytes <- A.take n
+    if B.all isHexDigit' bytes then return $ B.foldl' step 0 $ bytes else fail "pSizedHexadecimal"
+  where 
+    isHexDigit' w = (w >= 48 && w <= 57) ||  (w >= 97 && w <= 102) ||(w >= 65 && w <= 70)
+    step a w | w >= 48 && w <= 57  = (a `shiftL` 4) .|. fromIntegral (w - 48)
+             | w >= 97             = (a `shiftL` 4) .|. fromIntegral (w - 87)
+             | otherwise           = (a `shiftL` 4) .|. fromIntegral (w - 55)
+
+pPercentEnc :: Parser Char
+pPercentEnc = do
+  _ <- char '%'
+  hexdig1 <- pSizedHexadecimal 1
+  hexdig2 <- pSizedHexadecimal 1
+  return . chr $ hexdig1 * 16 + hexdig2
+
+pUsername :: Parser Username
+pUsername = do
+  xs <- many1' (satisfy isUsernameChar <|> pPercentEnc)
+  return $ Username (repack xs)
+
+pPassword :: Parser Password
+pPassword = do
+  xs <- many1' (satisfy isPasswordChar <|> pPercentEnc)
+  return $ Password (repack xs)
+
+pRegName :: Parser T.Text
+pRegName = do
+  xs <- many1' (satisfy isRegNameChar <|> pPercentEnc)
+  return . repack $ xs
+
+pIPv4 :: Parser T.Text
+pIPv4 = do
+  oct1 <- pOctet
+  _ <- char '.'
+  oct2 <- pOctet
+  _ <- char '.'
+  oct3 <- pOctet
+  _ <- char '.'
+  oct4 <- pOctet
+  return . T.pack $ show oct1 <> "." <> show oct2 <> "." <> show oct3 <> "." <> show oct4
+  where
+    pOctet :: Parser Int
+    pOctet = decimal >>= \case
+      i | i > 255 -> fail "IPv4 Octects must be in range 0-255"
+      i           -> return i
+
+pHost :: Parser Host
+pHost = do
+  hostText <- pIPv4 <|> pRegName
+  return . Host  $ hostText
+
+pUserInfo :: Parser UserInfo
+pUserInfo = do
+  username <- pUsername
+  password <- pMaybe (char ':' *> pPassword)
+  _ <- char '@'
+  return $ UserInfo { userInfoUsername = username, userInfoPassword = password }
+
+pPort :: Parser Int
+pPort = 
+  (char ':' *> decimal) >>= \case
+    i | i > 65535 -> fail "Port must be in the range 0-65535"
+    i             -> return i
+
+pAuthority :: Parser Authority
+pAuthority = do
+  _ <- string "//"
+  authUserInfo <- pMaybe pUserInfo
+  authHost <- pHost
+  authPort <- pMaybe pPort
+  _ <- peekChar >>= \case
+    Nothing                                   -> return ()
+    Just c | c == '/' || c == '?' || c == '#' -> return ()
+    _                                         -> fail "Invalid authority termination character, must be /, ?, # or end of input"
+  return Authority { authorityUserInfo = authUserInfo, authorityHost = authHost, authorityPort = authPort}
+
+pPathChar :: Parser Char 
+pPathChar = satisfy isPathChar <|> pPercentEnc
+
+pPathCharNc :: Parser Char 
+pPathCharNc = satisfy isPathCharNoColon <|> pPercentEnc
+
+pSegmentNz :: Parser PathSegment 
+pSegmentNz = PathSegment . repack <$> many1' pPathChar
+
+pSegmentNzNc :: Parser PathSegment 
+pSegmentNzNc = PathSegment . repack <$> many1' pPathCharNc
+
+pSegment :: Parser PathSegment
+pSegment = PathSegment . repack <$> many' pPathChar
+
+pPathsAbEmpty :: Parser [PathSegment]
+pPathsAbEmpty = many1' (char '/' *> pSegment)
+
+pPathsAbsolute :: Parser [PathSegment]
+pPathsAbsolute = do
+  _ <- char '/'
+  seg <- pSegmentNz
+  comps <- many' (char '/' *> pSegment)
+  return $ seg : comps
+
+pPathsNoScheme :: Parser [PathSegment]
+pPathsNoScheme = do
+  seg <- pSegmentNzNc
+  comps <- many' (char '/' *> pSegment)
+  return $ seg : comps
+
+pPathsRootless :: Parser [PathSegment]
+pPathsRootless = do
+  seg <- pSegmentNz
+  comps <- many' (char '/' *> pSegment)
+  return $ seg : comps
+
+pPathsEmpty :: Parser [PathSegment]
+pPathsEmpty = return []
+
+pPathAbsAuth :: Parser (Path 'Absolute)
+pPathAbsAuth = fmap Path (pPathsAbEmpty <|> pPathsAbsolute <|> pPathsEmpty)
+
+pPathAbsNoAuth :: Parser (Path 'Absolute)
+pPathAbsNoAuth = fmap Path (pPathsAbsolute <|> pPathsRootless <|> pPathsEmpty)
+
+pPathRel :: Parser (Path 'Relative)
+pPathRel = fmap Path (pPathsAbsolute <|> pPathsNoScheme <|> pPathsEmpty)
+
+pQuery :: Parser Query
+pQuery = do
+  queryText <- (char '?' *> (many1' (satisfy isQueryChar <|> pPercentEnc)))
+  _ <- peekChar >>= \case
+    Nothing           -> return ()
+    Just c | c == '#' -> return ()
+    c                 -> fail $ "Invalid query termination character: " <> show c <> ", must be # or end of input"
+  return . Query . repack $ queryText
+
+pFragment :: Parser Fragment
+pFragment = do
+  fragmentText <- (char '#' *> (many1' (satisfy isFragmentChar <|> pPercentEnc)))
+  _ <- peekChar >>= \case
+    Nothing           -> return ()
+    c                 -> fail $ "Invalid fragment termination character: " <> show c <> ", must be end of input"
+  return . Fragment . repack $ fragmentText
+
+pScheme :: Parser Scheme
+pScheme = do
+  x <- pAsciiAlpha
+  xs <- many' (pAsciiAlphaNumeric <|> char '+' <|> char '.' <|> char '-' )
+  _ <- char ':'
+  return $ Scheme (T.toLower . repack $ x:xs)
+
+pAbsolutePart :: Parser (Scheme, Maybe Authority)
+pAbsolutePart = do
+  scheme <- pScheme
+  authority <- pMaybe pAuthority
+  return (scheme, authority)
+
+pRelativeUri :: Parser Uri
+pRelativeUri = do
+  path <- pPathRel
+  query <- pMaybe pQuery
+  fragment <- pMaybe pFragment
+  _ <- endOfInput
+  return $ RelativeUri $ RelUri { uriPath = path, uriQuery = query, uriFragment = fragment }
+
+pAbsoluteUri :: Parser Uri
+pAbsoluteUri = do
+  (scheme, authority) <- pAbsolutePart
+  path <- if isJust authority then pPathAbsAuth else pPathAbsNoAuth
+  query <- pMaybe pQuery
+  fragment <- pMaybe pFragment
+  _ <- endOfInput
+  return $ AbsoluteUri $ AbsUri {uriScheme = scheme, uriAuthority = authority, uriPath = path, uriQuery = query, uriFragment = fragment }
+
+pUri :: Parser Uri
+pUri = pAbsoluteUri <|> pRelativeUri
diff --git a/src/Dormouse/Uri/QQ.hs b/src/Dormouse/Uri/QQ.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Uri/QQ.hs
@@ -0,0 +1,27 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE DataKinds #-}
+
+module Dormouse.Uri.QQ
+  ( uri
+  ) where
+  
+import Data.ByteString.Char8 (pack)
+import Dormouse.Uri
+import Language.Haskell.TH.Quote 
+import Language.Haskell.TH 
+
+uri :: QuasiQuoter
+uri = QuasiQuoter 
+  { quoteExp = \s -> 
+      let res = parseUri $ pack s in
+      case res of
+        Left err -> fail $ show err
+        Right x -> [| x :: Uri |]
+  , quotePat = \s ->
+      case parseUri (pack s) of
+        Left err -> fail $ show err
+        Right x  -> appE [|(==)|] [| (x :: Uri) |] `viewP` [p|True|]
+  , quoteType = error "Not supported"
+  , quoteDec =error "Not supported"
+  }
diff --git a/src/Dormouse/Uri/RFC3986.hs b/src/Dormouse/Uri/RFC3986.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Uri/RFC3986.hs
@@ -0,0 +1,74 @@
+module Dormouse.Uri.RFC3986
+  ( isGenDelim
+  , isSubDelim
+  , isReserved
+  , isAsciiAlpha
+  , isAsciiAlphaNumeric
+  , isUnreserved
+  , isSchemeChar
+  , isUsernameChar
+  , isPasswordChar
+  , isRegNameChar
+  , isPathChar
+  , isPathCharNoColon
+  , isQueryChar
+  , isFragmentChar
+  ) where
+
+import Data.Char as C
+
+-- | Checks whether a char belongs to the gen-delims in RFC3986 
+isGenDelim :: Char -> Bool
+isGenDelim c = c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'
+
+-- | Checks whether a char belongs to the sub-delims in RFC3986 
+isSubDelim :: Char -> Bool
+isSubDelim c = c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || c == '='
+
+-- | Checks whether a char belongs to the sub-delims in RFC3986 
+isReserved :: Char -> Bool
+isReserved c = isGenDelim c || isSubDelim c
+
+-- | Checks whether a char is ascii & alpha
+isAsciiAlpha :: Char -> Bool
+isAsciiAlpha c = C.isAlpha c && C.isAscii c
+
+-- | Checks whether a char is ascii & alphanumeric
+isAsciiAlphaNumeric :: Char -> Bool
+isAsciiAlphaNumeric c = C.isAlphaNum c && C.isAscii c
+
+-- | Checks whether a char belongs to the unreserved group in RFC3986 
+isUnreserved :: Char -> Bool
+isUnreserved c = isAsciiAlphaNumeric c || c == '-' || c == '.' || c == '_' || c == '~'
+
+-- | Checks whether a char is a valid scheme char in RFC3986
+isSchemeChar :: Char -> Bool
+isSchemeChar c = isAsciiAlphaNumeric c || c == '+' || c == '.' || c == '-'
+
+-- | Checks whether a char is a valid username char in RFC3986
+isUsernameChar :: Char -> Bool
+isUsernameChar c = isUnreserved c || isSubDelim c
+
+-- | Checks whether a char is a valid password char in RFC3986
+isPasswordChar :: Char -> Bool
+isPasswordChar c = isUnreserved c || isSubDelim c || c == ':'
+
+-- | Checks whether a char is a valid reg-name char in RFC3986
+isRegNameChar :: Char -> Bool
+isRegNameChar c = isUnreserved c || isSubDelim c
+
+-- | Checks whether a char is a valid path char in RFC3986
+isPathChar :: Char -> Bool
+isPathChar c = isUnreserved c || isSubDelim c || c == ':' || c == '@'
+
+-- | Checks whether a char is a valid path char (excluding colons) in RFC3986
+isPathCharNoColon :: Char -> Bool
+isPathCharNoColon c = isUnreserved c || isSubDelim c || c == '@'
+
+-- | Checks whether a char is a valid query char in RFC3986
+isQueryChar :: Char -> Bool
+isQueryChar c = isPathChar c || c == '?' || c == '/'
+
+-- | Checks whether a char is a valid fragment char in RFC3986
+isFragmentChar :: Char -> Bool
+isFragmentChar c = isPathChar c || c == '?' || c == '/'
diff --git a/src/Dormouse/Uri/Types.hs b/src/Dormouse/Uri/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Uri/Types.hs
@@ -0,0 +1,134 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE DeriveLift #-}
+
+module Dormouse.Uri.Types
+  ( UriReference(..)
+  , Authority(..)
+  , Fragment(..)
+  , Host(..)
+  , Path(..)
+  , PathSegment(..)
+  , Query(..)
+  , Scheme(..)
+  , Username(..)
+  , Password(..)
+  , UserInfo(..)
+  , Uri(..)
+  , AbsUri(..)
+  , RelUri(..)
+  ) where
+
+import Data.String (IsString(..))
+import qualified Data.List as L
+import Data.Text (Text, unpack, pack)
+import Language.Haskell.TH.Syntax (Lift(..))
+
+-- | The Username subcomponent of a URI UserInfo
+newtype Username = Username { unUsername :: Text } deriving (Eq, Lift)
+
+instance Show Username where
+  show username = unpack $ unUsername username
+
+instance IsString Username where
+  fromString s = Username $ pack s
+
+-- | The Password subcomponent of a URI UserInfo
+newtype Password = Password { unPassword :: Text } deriving (Eq, Lift)
+
+instance Show Password where
+  show _ = "****"
+
+instance IsString Password where
+  fromString s = Password $ pack s
+
+-- | The UserInfo subcomponent of a URI Authority
+data UserInfo = UserInfo 
+  { userInfoUsername :: Username
+  , userInfoPassword :: Maybe Password
+  } deriving (Eq, Show, Lift)
+
+-- | The Host subcomponent of a URI Authority
+newtype Host = Host { unHost :: Text } deriving (Eq, Lift)
+
+instance IsString Host where
+  fromString s = Host $ pack s
+
+instance Show Host where
+  show host = unpack $ unHost host
+
+-- | The Authority component of a URI
+data Authority = Authority 
+  { authorityUserInfo :: Maybe UserInfo
+  , authorityHost :: Host
+  , authorityPort :: Maybe Int
+  } deriving (Eq, Show, Lift)
+
+-- | The Fragment component of a URI
+newtype Fragment = Fragment { unFragment :: Text } deriving (Eq, Lift)
+
+instance IsString Fragment where
+  fromString s = Fragment $ pack s
+
+instance Show Fragment where
+  show fragment = unpack $ unFragment fragment
+
+data UriReference = Absolute | Relative
+
+-- | The Path component of a URI, including a series of individual Path Segments
+newtype Path (ref :: UriReference) = Path { unPath :: [PathSegment]} deriving (Eq, Lift)
+
+instance Show (Path ref) where
+  show path = "[" <> L.intercalate "," (fmap show . unPath $ path) <> "]"
+
+-- | An individial Path segment of a URI
+newtype PathSegment = PathSegment { unPathSegment :: Text } deriving (Eq, Lift)
+
+instance IsString PathSegment where
+  fromString s = PathSegment $ pack s
+
+instance Show PathSegment where
+  show seg = unpack $ unPathSegment seg
+
+-- | The Query component of a URI
+newtype Query = Query { unQuery :: Text } deriving (Eq, Lift)
+
+instance IsString Query where
+  fromString s = Query $ pack s
+
+instance Show Query where
+  show query = unpack $ unQuery query
+
+-- | The Scheme component of a URI
+newtype Scheme = Scheme { unScheme :: Text } deriving (Eq, Lift)
+
+instance Show Scheme where
+  show scheme = unpack . unScheme $ scheme
+
+-- | The data associated with an Absolute URI
+data AbsUri = AbsUri
+  { uriScheme :: Scheme
+  , uriAuthority :: Maybe Authority
+  , uriPath :: Path 'Absolute
+  , uriQuery :: Maybe Query
+  , uriFragment :: Maybe Fragment
+  } deriving (Eq, Show, Lift)
+
+-- | The data associated with a Relative URI
+data RelUri = RelUri
+  { uriPath :: Path 'Relative
+  , uriQuery :: Maybe Query
+  , uriFragment :: Maybe Fragment
+  } deriving (Eq, Show, Lift)
+
+-- | A Uniform Resource Identifier (URI) is a compact sequence of characters that identifies an abstract or physical resource.
+-- It is defined according to RFC 3986 (<https://tools.ietf.org/html/rfc3986>).  URIs can be absolute (i.e. defined against a
+-- specific scheme) or relative.
+data Uri 
+  = AbsoluteUri AbsUri
+  | RelativeUri RelUri
+  deriving (Lift, Eq, Show)
+
+
diff --git a/src/Dormouse/Url.hs b/src/Dormouse/Url.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Url.hs
@@ -0,0 +1,60 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE KindSignatures #-}
+
+module Dormouse.Url
+  ( module Dormouse.Url.Types
+  , ensureHttp
+  , ensureHttps
+  , parseUrl
+  , parseHttpUrl
+  , parseHttpsUrl
+  , IsUrl(..)
+  ) where
+
+import Control.Exception.Safe (MonadThrow, throw)
+import qualified Data.ByteString as SB
+import qualified Data.Text as T
+import Dormouse.Url.Exception (UrlException(..))
+import Dormouse.Uri
+import Dormouse.Url.Class
+import Dormouse.Url.Types
+
+-- | Ensure that the supplied Url uses the _http_ scheme, throwing a 'UrlException' in @m@ if this is not the case
+ensureHttp :: MonadThrow m => AnyUrl -> m (Url "http")
+ensureHttp (AnyUrl (HttpUrl u)) = return $ HttpUrl u
+ensureHttp (AnyUrl (HttpsUrl _)) = throw $ UrlException "Supplied url was an https url, not an http url"
+
+-- | Ensure that the supplied Url uses the _https_ scheme, throwing a 'UrlException' in @m@ if this is not the case
+ensureHttps :: MonadThrow m => AnyUrl -> m (Url "https")
+ensureHttps (AnyUrl (HttpsUrl u)) = return $ HttpsUrl u
+ensureHttps (AnyUrl (HttpUrl _)) = throw $ UrlException "Supplied url was an http url, not an https url"
+
+-- | Ensure that the supplied Uri is a Url
+ensureUrl :: MonadThrow m => Uri -> m AnyUrl
+ensureUrl (AbsoluteUri AbsUri {uriScheme = scheme, uriAuthority = maybeAuthority, uriPath = path, uriQuery = query, uriFragment = fragment}) = do
+  authority <- maybe (throw $ UrlException "Supplied Url had no authority component") return maybeAuthority
+  case unScheme scheme of
+    "http"  -> return $ AnyUrl $ HttpUrl UrlComponents { urlAuthority = authority, urlPath = path, urlQuery = query, urlFragment = fragment }
+    "https" -> return $ AnyUrl $ HttpsUrl UrlComponents { urlAuthority = authority, urlPath = path, urlQuery = query, urlFragment = fragment }
+    s       -> throw $ UrlException ("Supplied Url had a scheme of " <> T.pack (show s) <> " which was not http or https.")
+ensureUrl (RelativeUri _) = throw $ UrlException "Supplied Uri was a relative Uri - it must provide a scheme and authority to be considered a valid url"
+
+-- | Parse an ascii 'ByteString' as a url, throwing a 'UriException' in @m@ if this fails
+parseUrl :: MonadThrow m => SB.ByteString -> m AnyUrl
+parseUrl bs = do
+  url <- parseUri bs
+  ensureUrl url
+  
+-- | Parse an ascii 'ByteString' as an http url, throwing a 'UriException' in @m@ if this fails
+parseHttpUrl :: MonadThrow m => SB.ByteString -> m (Url "http")
+parseHttpUrl text = do 
+  anyUrl <- parseUrl text
+  ensureHttp anyUrl
+
+-- | Parse an ascii 'ByteString' as an https url, throwing a 'UriException' in @m@ if this fails
+parseHttpsUrl :: MonadThrow m => SB.ByteString -> m (Url "https")
+parseHttpsUrl text = do 
+  anyUrl <- parseUrl text
+  ensureHttps anyUrl
diff --git a/src/Dormouse/Url/Builder.hs b/src/Dormouse/Url/Builder.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Url/Builder.hs
@@ -0,0 +1,78 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE RecordWildCards #-}
+  
+-- | Provides syntax for incrementally building Urls and components of Urls such as Query strings
+module Dormouse.Url.Builder 
+  ( QueryComponent(..)
+  , QueryBuilder(..)
+  , IsQueryVal(..)
+  ,  (</>)
+  , (?)
+  , (&)
+  , (=:)
+  ) where
+
+import Data.Foldable  
+import Data.Text (Text)
+import Dormouse.Uri.Types
+import Dormouse.Url.Types
+
+import qualified Data.Text as T
+import qualified Data.Sequence as SQ
+
+-- | An individual Query Parameter or Query Flag
+data QueryComponent
+  = QueryParam T.Text T.Text
+  | QueryFlag T.Text
+  deriving (Eq, Show)
+
+-- | Stores a series of Query Parameters or Query Flags used for building the Query component of a URI/URL, for example in the format @?param1=2&param2=3&flag1@
+newtype QueryBuilder = QueryBuilder { unQueryBuilder :: SQ.Seq QueryComponent }
+  deriving (Eq, Show)
+
+instance Semigroup QueryBuilder where
+  x1 <> x2 = QueryBuilder $ unQueryBuilder x1 <> unQueryBuilder x2
+
+instance Monoid QueryBuilder where
+  mempty = QueryBuilder SQ.empty
+
+-- | Signifies a type that can be rendered as Query Parameter within a URI
+class IsQueryVal a where
+  toQueryVal :: a -> T.Text
+
+instance IsQueryVal Bool where toQueryVal = T.pack . show
+instance IsQueryVal Char where toQueryVal = T.pack . show
+instance IsQueryVal Double where toQueryVal = T.pack . show
+instance IsQueryVal Float where toQueryVal = T.pack . show
+instance IsQueryVal Int where toQueryVal = T.pack . show
+instance IsQueryVal Integer where toQueryVal = T.pack . show
+instance IsQueryVal String where toQueryVal = T.pack
+instance IsQueryVal T.Text where toQueryVal = id
+
+-- | Combine a Url with a new text path component
+(</>) :: Url scheme -> Text -> Url scheme
+(</>) (HttpUrl UrlComponents {urlPath = path, .. }) text = HttpUrl $ UrlComponents {urlPath = (Path {unPath = unPath path ++ [PathSegment text] }), ..}
+(</>) (HttpsUrl UrlComponents {urlPath = path, .. }) text = HttpsUrl $ UrlComponents {urlPath = (Path {unPath = unPath path ++ [PathSegment text] }), ..}
+
+-- | Convenient alias for '<>' which allows for combining query parameters
+(&) :: QueryBuilder -> QueryBuilder -> QueryBuilder
+(&) = (<>)
+
+-- | Combine a Url with a some supplied query parameters
+(?) :: Url scheme -> QueryBuilder -> Url scheme
+(?) uri b = 
+  case uri of 
+    HttpUrl  UrlComponents { .. } -> HttpUrl $ UrlComponents { urlQuery = Just $ foldl' folder "" $ unQueryBuilder b  , .. }
+    HttpsUrl UrlComponents { .. } -> HttpsUrl $ UrlComponents { urlQuery = Just $ foldl' folder "" $ unQueryBuilder b  , .. }
+  where 
+    folder "" (QueryFlag val)       = Query $ val
+    folder "" (QueryParam key val)  = Query $ key <> "=" <> val
+    folder acc (QueryFlag val)      = Query $ unQuery acc <> "&" <> val
+    folder acc (QueryParam key val) = Query $ unQuery acc <> "&" <> key <> "=" <> val
+
+infixl 8 ?
+
+-- | Generate a query paramter of the form @key=value@
+(=:) :: IsQueryVal a => Text -> a -> QueryBuilder
+(=:) key value = QueryBuilder . SQ.singleton $ QueryParam key (toQueryVal value)
diff --git a/src/Dormouse/Url/Class.hs b/src/Dormouse/Url/Class.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Url/Class.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE GADTs #-}
+
+module Dormouse.Url.Class 
+  ( IsUrl(..)
+  ) where
+
+import Dormouse.Url.Types
+
+class (Eq url, Show url) => IsUrl url where
+  asAnyUrl :: url -> AnyUrl
+
+instance IsUrl (Url scheme) where
+  asAnyUrl = AnyUrl
+
+instance IsUrl AnyUrl where
+  asAnyUrl (AnyUrl u) = asAnyUrl u
diff --git a/src/Dormouse/Url/Exception.hs b/src/Dormouse/Url/Exception.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Url/Exception.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE ExistentialQuantification #-}
+
+module Dormouse.Url.Exception
+  ( UrlException(..)
+  ) where
+
+import Control.Exception.Safe (Exception(..))
+import qualified Data.Text as T
+
+-- | 'UrlException' is used to indicate an error in transforming a valid URI into a URL, e.g. the URI refers to a different schema such as @file@
+newtype UrlException = UrlException  { urlExceptionMessage :: T.Text }
+
+instance Show UrlException where
+  show UrlException { urlExceptionMessage = msg } = "Failed to parse url: " <> T.unpack msg
+
+instance Exception UrlException
diff --git a/src/Dormouse/Url/QQ.hs b/src/Dormouse/Url/QQ.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Url/QQ.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE DataKinds #-}
+
+module Dormouse.Url.QQ
+  ( http
+  , https
+  , url
+  ) where
+
+import Data.ByteString.Char8 (pack)
+import Dormouse.Url
+import Language.Haskell.TH.Quote 
+import Language.Haskell.TH
+
+http :: QuasiQuoter
+http = QuasiQuoter 
+  { quoteExp = \s -> 
+      let res = parseHttpUrl $ pack s in
+      case res of
+        Left err -> fail $ show err
+        Right x -> [| x |]
+  , quotePat = \s ->
+      case parseHttpUrl (pack s) of
+        Left err -> fail $ show err
+        Right x  -> appE [|(==)|] [| (x :: Url "http") |] `viewP` [p|True|]
+  , quoteType = error "Not supported"
+  , quoteDec =error "Not supported"
+  }
+
+https :: QuasiQuoter
+https = QuasiQuoter 
+  { quoteExp = \s -> 
+      let res = parseHttpsUrl $ pack s in
+      case res of
+        Left err -> fail $ show err
+        Right x -> [| (x :: Url "https") |]
+  , quotePat = \s ->
+      case parseHttpsUrl (pack s) of
+        Left err -> fail $ show err
+        Right x  -> appE [|(==)|] [| (x :: Url "https") |] `viewP` [p|True|]
+  , quoteType = error "Not supported"
+  , quoteDec =error "Not supported"
+  }
+
+url :: QuasiQuoter
+url = QuasiQuoter 
+  { quoteExp = \s -> 
+      let res = parseUrl $ pack s in
+      case res of
+        Left err -> fail $ show err
+        Right x -> [| (x :: AnyUrl) |]
+  , quotePat = \s ->
+      case parseUrl (pack s) of
+        Left err -> fail $ show err
+        Right x  -> appE [|(==)|] [| (x :: AnyUrl) |] `viewP` [p|True|]
+  , quoteType = error "Not supported"
+  , quoteDec =error "Not supported"
+  }
diff --git a/src/Dormouse/Url/Types.hs b/src/Dormouse/Url/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Dormouse/Url/Types.hs
@@ -0,0 +1,61 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE DeriveLift #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Dormouse.Url.Types
+  ( UrlComponents(..)
+  , UrlScheme(..)
+  , Url(..)
+  , AnyUrl(..)
+  ) where
+
+import Dormouse.Uri.Types
+import GHC.TypeLits
+import Language.Haskell.TH.Syntax (Lift(..))
+
+data UrlComponents = UrlComponents
+  { urlAuthority :: Authority
+  , urlPath :: Path 'Absolute
+  , urlQuery :: Maybe Query
+  , urlFragment :: Maybe Fragment
+  } deriving (Eq, Show, Lift)
+
+data UrlScheme
+  = HttpScheme
+  | HttpsScheme
+
+-- | A 'Url' is defined here as an absolute URI in the @http@ or @https@ schemes.  Authority components are requried by the http / https
+-- Uri schemes.
+data Url (scheme :: Symbol) where
+  HttpUrl  :: UrlComponents -> Url "http"
+  HttpsUrl :: UrlComponents -> Url "https"
+
+instance Eq (Url scheme) where
+  (==) (HttpUrl u1)  (HttpUrl u2)  = show u1 == show u2
+  (==) (HttpsUrl u1) (HttpsUrl u2) = show u1 == show u2
+
+instance Show (Url scheme) where
+  show (HttpUrl wu)  = "http " <> show wu
+  show (HttpsUrl wu) = "https " <> show wu
+
+instance Lift (Url scheme) where
+  lift (HttpUrl uc)  = [| HttpUrl uc |]
+  lift (HttpsUrl uc) = [| HttpsUrl uc |]
+
+-- | `AnyUrl` is a wrapper aroud `Url` which allows either @http@ or @https@ urls.
+data AnyUrl = forall scheme. AnyUrl (Url scheme)
+
+instance Eq AnyUrl where
+  (==) (AnyUrl (HttpUrl d1)) (AnyUrl (HttpUrl d2))   = d1 == d2
+  (==) (AnyUrl (HttpsUrl d1)) (AnyUrl (HttpsUrl d2)) = d1 == d2
+  (==) _  _                                          = False
+
+instance Show AnyUrl where
+  show (AnyUrl u) = show u
+
+instance Lift AnyUrl where
+  lift (AnyUrl u)  = [| AnyUrl u |]
diff --git a/test/Dormouse/Generators/UriComponents.hs b/test/Dormouse/Generators/UriComponents.hs
new file mode 100644
--- /dev/null
+++ b/test/Dormouse/Generators/UriComponents.hs
@@ -0,0 +1,246 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Dormouse.Generators.UriComponents 
+  ( genValidScheme
+  , genInvalidScheme
+  , genValidUsername
+  , genInvalidUsername
+  , genValidPassword
+  , genInvalidPassword
+  , genValidUserInfo
+  , genInvalidUserInfo
+  , genValidIPv4
+  , genValidRegName
+  , genValidHost
+  , genValidPort
+  , genValidAuthority
+  , genValidPathAbsAuth
+  , genValidPathAbsNoAuth
+  , genValidPathRel
+  , genValidQuery
+  , genValidFragment
+  , genValidAbsoluteUri
+  )
+  where
+
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as B8
+import Data.ByteString.Internal (c2w, w2c)
+import qualified Data.Char as C
+import qualified Data.Text as T
+import Dormouse.Uri.Encode
+import Dormouse.Uri.RFC3986
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+genPercentEncoded :: Gen B.ByteString
+genPercentEncoded = do
+  char <- Gen.filter C.isPrint Gen.unicode
+  let t = T.pack $ [char]
+  let percentEncoded = encodeUnless (const False) t
+  return $ percentEncoded
+
+genValidScheme :: Gen B.ByteString
+genValidScheme = do
+  first <- Gen.filter isAsciiAlpha Gen.ascii
+  bs <- Gen.list (Range.constant 0 10) (Gen.filter isSchemeChar Gen.ascii)
+  return . B8.pack $ (first : bs ++ [':'])
+
+data SchemeFailureMode 
+  = NonAsciiFirstChar
+  | InvalidSchemeChar
+  | NoTrailingSemicolon
+
+schemeFailureMode :: Int -> SchemeFailureMode
+schemeFailureMode 1 = NonAsciiFirstChar
+schemeFailureMode 2 = InvalidSchemeChar
+schemeFailureMode 3 = NoTrailingSemicolon
+schemeFailureMode _ = undefined
+
+genInvalidScheme :: Gen B.ByteString
+genInvalidScheme = do
+  failureMode <- fmap schemeFailureMode $ Gen.element [1..3]
+  first <- case failureMode of
+    NonAsciiFirstChar -> Gen.filter (not . isAsciiAlpha) Gen.ascii
+    _                 -> Gen.filter isAsciiAlpha Gen.ascii
+  remainder <- case failureMode of
+    InvalidSchemeChar -> do
+      let c = Gen.filter (\x -> (not $ isSchemeChar x) && x /= ':') Gen.ascii
+      Gen.list (Range.constant 1 10) c
+    _ -> do
+      let c = Gen.filter isSchemeChar Gen.ascii
+      Gen.list (Range.constant 0 10) c
+  let finalBs = case failureMode of
+        NoTrailingSemicolon -> first : remainder
+        _                   -> first : remainder ++ [':']
+  return $ B8.pack finalBs
+
+genUsernameChar :: Gen B.ByteString
+genUsernameChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isUsernameChar Gen.ascii)]
+
+genValidUsername :: Gen B.ByteString
+genValidUsername = do
+  list <- Gen.list (Range.constant 1 20) genUsernameChar
+  return $ B.intercalate "" list
+
+genInvalidUsername :: Gen B.ByteString
+genInvalidUsername = do
+  invalids <- Gen.list (Range.constant 1 5) genInvalidUsernameChar
+  valids <- Gen.list (Range.constant 0 15) genUsernameChar
+  fmap (B.intercalate "") $ Gen.shuffle $ invalids ++ valids
+  where
+    genInvalidUsernameChar = fmap (B8.pack . return) $ Gen.filter (\x -> (not $ isUsernameChar x) && x /= '%'  && C.isPrint x) Gen.ascii
+
+genPasswordChar :: Gen B.ByteString
+genPasswordChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isPasswordChar Gen.ascii)]
+
+genValidPassword :: Gen B.ByteString
+genValidPassword = do
+  list <- Gen.list (Range.constant 1 20) genPasswordChar
+  return $ B.intercalate "" list
+
+genInvalidPassword :: Gen B.ByteString
+genInvalidPassword = do
+  invalids <- Gen.list (Range.constant 1 5) genInvalidPasswordChar
+  valids <- Gen.list (Range.constant 0 15) genPasswordChar
+  fmap (B.intercalate "") $ Gen.shuffle $ invalids ++ valids
+  where
+    genInvalidPasswordChar = fmap (B8.pack . return) $ Gen.filter (\x -> (not $ isPasswordChar x) && x /= '%' && C.isPrint x) Gen.ascii
+
+genValidUserInfo :: Gen B.ByteString
+genValidUserInfo = do
+  username <- genValidUsername
+  maybePassword <- Gen.maybe genValidPassword
+  let passwordSuffix = maybe B.empty (B.cons $ c2w ':') maybePassword
+  return $ B.append (B.append username passwordSuffix) "@"
+
+data UserInfoFailureMode 
+  = InvalidUsername
+  | InvalidPassword
+  | MissingAtSuffix
+
+userInfoFailureMode :: Int -> UserInfoFailureMode
+userInfoFailureMode 1 = InvalidUsername
+userInfoFailureMode 2 = InvalidPassword
+userInfoFailureMode 3 = MissingAtSuffix
+userInfoFailureMode _ = undefined
+
+genInvalidUserInfo :: Gen B.ByteString
+genInvalidUserInfo = do
+  failureMode <- fmap userInfoFailureMode $ Gen.element [1..3]
+  username <- case failureMode of
+    InvalidUsername -> Gen.filter (B.all (\x -> w2c x /= ':')) genInvalidUsername -- if the username is supposed to be invalid, ensure that ':' is not present, otherwise the user info could be interpreted as valid if valid chars precede the ':'
+    _               -> genValidUsername
+  maybePassword <- case failureMode of 
+    InvalidPassword -> fmap Just genInvalidPassword
+    _               -> Gen.maybe $ genValidPassword
+  let passwordSuffix = maybe B.empty (B.cons $ c2w ':') maybePassword
+  let complete = case failureMode of
+        MissingAtSuffix -> B.append (B.append username passwordSuffix) "#"
+        _               -> B.append (B.append username passwordSuffix) "@"
+  return complete
+
+genValidIPv4 :: Gen B.ByteString
+genValidIPv4 = do
+  ipChars <- (\a b c d -> show a <> "." <> show b <> "." <> show c <> "." <> show d) <$> genOctet <*> genOctet <*> genOctet <*> genOctet
+  return $ B8.pack ipChars
+  where
+    genOctet = Gen.word8 Range.constantBounded
+
+genValidRegName :: Gen B.ByteString
+genValidRegName = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isRegNameChar Gen.ascii)]
+
+genValidHost :: Gen B.ByteString
+genValidHost = Gen.choice [genValidIPv4, genValidRegName]
+
+genValidPort :: Gen B.ByteString
+genValidPort = fmap (B.append ":" . B8.pack . show) $ Gen.word16 Range.constantBounded
+
+genValidAuthority :: Gen B.ByteString
+genValidAuthority = do
+  maybeUserInfo <- Gen.maybe genValidUserInfo
+  let userInfoPrefix = maybe B.empty id maybeUserInfo
+  host <- genValidHost
+  maybePort <- Gen.maybe genValidPort
+  let portSuffix = maybe B.empty id maybePort
+  return . B.append "//" . B.append userInfoPrefix $ B.append host portSuffix
+
+genPathChar :: Gen B.ByteString
+genPathChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isPathChar Gen.ascii)]
+
+genPathSegment :: Gen B.ByteString
+genPathSegment = fmap (B.intercalate "") $ Gen.list (Range.constant 0 20) genPathChar
+
+genPathSegmentNz :: Gen B.ByteString
+genPathSegmentNz = fmap (B.intercalate "") $ Gen.list (Range.constant 1 20) genPathChar
+
+genPathCharNc :: Gen B.ByteString
+genPathCharNc = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isPathCharNoColon Gen.ascii)]
+
+genPathSegmentNzNc :: Gen B.ByteString
+genPathSegmentNzNc = fmap (B.intercalate "") $ Gen.list (Range.constant 1 20) genPathCharNc
+
+genPathsAbEmpty :: Gen B.ByteString
+genPathsAbEmpty = do
+  components <- Gen.list (Range.constant 1 10) genPathSegment
+  return . B.append "/" $ B.intercalate "/" components
+
+genPathsAbsolute :: Gen B.ByteString
+genPathsAbsolute = do
+  first <- genPathSegmentNz
+  components <- Gen.list (Range.constant 0 10) genPathSegment
+  return . B.append "/" . B.append first $ B.intercalate "/" components
+
+genPathsNoScheme :: Gen B.ByteString
+genPathsNoScheme = do
+  first <- genPathSegmentNzNc
+  components <- Gen.list (Range.constant 0 10) genPathSegment
+  return . B.append first . B.append "/" $ B.intercalate "/" components
+
+genPathsRootless :: Gen B.ByteString
+genPathsRootless = do
+  first <- genPathSegmentNz
+  components <- Gen.list (Range.constant 0 10) genPathSegment
+  return . B.append first . B.append "/" $ B.intercalate "/" components
+
+genValidPathsEmpty :: Gen B.ByteString
+genValidPathsEmpty = return B.empty
+
+genValidPathAbsAuth :: Gen B.ByteString
+genValidPathAbsAuth = Gen.choice [genPathsAbEmpty, genPathsAbsolute, genValidPathsEmpty]
+
+genValidPathAbsNoAuth :: Gen B.ByteString
+genValidPathAbsNoAuth = Gen.choice [genPathsAbsolute, genPathsRootless, genValidPathsEmpty]
+
+genValidPathRel :: Gen B.ByteString
+genValidPathRel = Gen.choice [genPathsAbsolute, genPathsNoScheme, genValidPathsEmpty]
+
+genQueryChar :: Gen B.ByteString
+genQueryChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isQueryChar Gen.ascii)]
+
+genValidQuery :: Gen B.ByteString
+genValidQuery = do
+  list <- Gen.list (Range.constant 1 50) genQueryChar
+  return $ B.append "?" $ B.intercalate "" list
+
+genFragmentChar :: Gen B.ByteString
+genFragmentChar = Gen.frequency [(1, genPercentEncoded), (25, fmap (B8.pack . return) $ Gen.filter isFragmentChar Gen.ascii)]
+
+genValidFragment :: Gen B.ByteString
+genValidFragment = do
+  list <- Gen.list (Range.constant 1 50) genFragmentChar
+  return $ B.append "#" $ B.intercalate "" list
+
+genValidAbsoluteUri :: Gen B.ByteString
+genValidAbsoluteUri = do
+  scheme <- genValidScheme
+  authority <- Gen.maybe genValidAuthority
+  path <- case authority of
+    Just _  -> genValidPathAbsAuth
+    Nothing -> genValidPathAbsNoAuth
+  query <- Gen.maybe genValidQuery
+  fragment <- Gen.maybe genValidFragment
+  return . B.intercalate "" $ [scheme, maybe B.empty id authority, path, maybe B.empty id query, maybe B.empty id fragment]
+
+
diff --git a/test/Dormouse/Uri/ParserSpec.hs b/test/Dormouse/Uri/ParserSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Dormouse/Uri/ParserSpec.hs
@@ -0,0 +1,268 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DisambiguateRecordFields #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE ViewPatterns #-}
+
+module Dormouse.Uri.ParserSpec
+  ( spec
+  ) where
+
+import Test.Hspec
+import Test.Hspec.Hedgehog
+import Control.Monad.IO.Class
+import Data.Attoparsec.ByteString.Char8
+import qualified Data.ByteString as B
+import Data.ByteString.Internal (c2w, w2c)
+import qualified Data.Char as C
+import Data.Either (isLeft, isRight)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as TE
+import Dormouse.Generators.UriComponents 
+import Dormouse.Uri.Types
+import Dormouse.Uri.Parser
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+
+uriWithHostAndPath :: Uri
+uriWithHostAndPath = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "http"
+  , uriAuthority = Just $ Authority {authorityUserInfo = Nothing, authorityHost = Host "google.com", authorityPort = Nothing}
+  , uriPath = Path [PathSegment "test1", PathSegment "test2"]
+  , uriQuery = Nothing
+  , uriFragment = Nothing
+  }
+
+uriWithHostUsernameAndPath :: Uri
+uriWithHostUsernameAndPath = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "http"
+  , uriAuthority = Just $ Authority 
+    { authorityUserInfo = Just (UserInfo {userInfoUsername = "j.t.kirk", userInfoPassword = Nothing})
+    , authorityHost = Host "google.com"
+    , authorityPort = Nothing
+    }
+  , uriPath = Path [PathSegment "test1", PathSegment "test2"]
+  , uriQuery = Nothing
+  , uriFragment = Nothing
+  }
+
+uriWithHostUsernamePasswordAndPath :: Uri
+uriWithHostUsernamePasswordAndPath = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "http"
+  , uriAuthority = Just $ Authority 
+    { authorityUserInfo = Just (UserInfo {userInfoUsername = "j.t.kirk", userInfoPassword = Just "11a"})
+    , authorityHost = Host "google.com"
+    , authorityPort = Nothing
+    }
+  , uriPath = Path [PathSegment "test1", PathSegment "test2"]
+  , uriQuery = Nothing
+  , uriFragment = Nothing
+  }
+
+uriWithHostPathAndPort :: Uri
+uriWithHostPathAndPort = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "https"
+  , uriAuthority = Just $ Authority { authorityUserInfo = Nothing, authorityHost = "www.example.com", authorityPort = Just 123 }
+  , uriPath = Path ["forum", "questions", ""]
+  , uriQuery = Nothing
+  , uriFragment = Nothing
+  }
+
+uriWithHostPathQueryAndFragment :: Uri
+uriWithHostPathQueryAndFragment = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "https"
+  , uriAuthority = Just $ Authority { authorityUserInfo = Nothing, authorityHost = "www.example.com", authorityPort = Nothing }
+  , uriPath = Path ["forum", "questions", ""]
+  , uriQuery = Just $ Query "tag=networking&order=newest"
+  , uriFragment = Just $ Fragment "top"
+  }
+
+uriWithUnicodeInQuery :: Uri
+uriWithUnicodeInQuery = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "https"
+  , uriAuthority = Just $ Authority { authorityUserInfo = Nothing, authorityHost = "www.example.com", authorityPort = Nothing }
+  , uriPath = Path ["forum", "questions", ""]
+  , uriQuery = Just $ Query "tag=networking&order=newest😀"
+  , uriFragment = Nothing
+  }
+
+uriWithSpacesInQuery :: Uri
+uriWithSpacesInQuery = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "https"
+  , uriAuthority = Just $ Authority { authorityUserInfo = Nothing, authorityHost = "www.example.com", authorityPort = Nothing }
+  , uriPath = Path ["forum", "questions", ""]
+  , uriQuery = Just $ Query "tag=with space"
+  , uriFragment = Nothing
+  }
+
+
+uriWithUnicodeInFragment :: Uri
+uriWithUnicodeInFragment = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "https"
+  , uriAuthority = Just $ Authority { authorityUserInfo = Nothing, authorityHost = "www.example.com", authorityPort = Nothing }
+  , uriPath = Path ["forum", "questions", ""]
+  , uriQuery = Nothing
+  , uriFragment = Just $ "😀😀😀"
+  }
+
+ldapUri :: Uri
+ldapUri = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "ldap"
+  , uriAuthority = Just $ Authority { authorityUserInfo = Nothing, authorityHost = "192.168.0.1", authorityPort = Nothing }
+  , uriPath = Path [PathSegment "c=GB"]
+  , uriQuery = Just $ Query "objectClass?one"
+  , uriFragment = Nothing
+  }
+
+telUri :: Uri
+telUri = AbsoluteUri $ AbsUri
+  { uriScheme = Scheme "tel"
+  , uriAuthority = Nothing
+  , uriPath = Path ["+1-816-555-1212"]
+  , uriQuery = Nothing
+  , uriFragment = Nothing
+  }
+
+infixr 5 :<
+
+pattern b :< bs <- (B.uncons -> Just (b, bs))
+pattern Empty   <- (B.uncons -> Nothing)
+
+percentDecode :: B.ByteString -> B.ByteString
+percentDecode Empty = B.empty
+percentDecode (x :< Empty) = B.singleton x
+percentDecode (x :< y :< Empty) = B.cons x $ B.singleton y
+percentDecode (p :< x :< y :< xs) 
+  | p == c2w '%' = B.cons (fromIntegral $ C.digitToInt (w2c x) * 16 + C.digitToInt (w2c y)) (percentDecode xs)
+  | otherwise    = B.cons p $ percentDecode (B.cons x $ B.cons y $ xs)
+
+spec :: Spec
+spec = do
+  describe "pScheme" $ do
+    it "returns the matching scheme for all valid schemes" $ hedgehog $ do
+      schemeText <- forAll genValidScheme
+      let res = parseOnly (pScheme <* endOfInput) schemeText
+      res === (Right . Scheme . T.init . T.toLower $ TE.decodeUtf8 schemeText)
+    it "fails for invalid schemes" $ hedgehog $ do
+      schemeText <- forAll genInvalidScheme
+      let res = parseOnly (pScheme <* endOfInput) schemeText
+      isLeft res === True
+  describe "pUsername" $ do
+    it "returns the matching username for all valid usernames" $ hedgehog $ do
+      usernameText <- forAll genValidUsername
+      let res = parseOnly (pUsername <* endOfInput) usernameText
+      res === (Right . Username . TE.decodeUtf8 . percentDecode $ usernameText)
+    it "fails for invalid usernames" $ hedgehog $ do
+      usernameText <- forAll genInvalidUsername
+      let res = parseOnly (pUsername <* endOfInput) usernameText
+      isLeft res === True
+  describe "pPassword" $ do
+    it "returns the matching password for all valid passwords" $ hedgehog $ do
+      passwordText <- forAll genValidPassword
+      let res = parseOnly (pPassword <* endOfInput) passwordText
+      res === (Right . Password . TE.decodeUtf8 . percentDecode $ passwordText)
+    it "fails for invalid passwords" $ hedgehog $ do
+      passwordText <- forAll genInvalidPassword
+      let res = parseOnly (pPassword <* endOfInput) passwordText
+      isLeft res === True
+  describe "pUserInfo" $ do
+    it "generates a user info for all valid user infos" $ hedgehog $ do
+      userInfoText <- forAll genValidUserInfo
+      let res = parseOnly (pUserInfo <* endOfInput) userInfoText
+      isRight res === True
+    it "fails for invalid user infos" $ hedgehog $ do
+      userInfoText <- forAll genInvalidUserInfo
+      let res = parseOnly (pUserInfo <* endOfInput) userInfoText
+      isLeft res === True
+  describe "pIPv4" $ do
+    it "returns the matching ip address for all valid ip addresses" $ hedgehog $ do
+      ipv4Text <- forAll genValidIPv4
+      let res = parseOnly (pIPv4 <* endOfInput) ipv4Text
+      res === (Right . TE.decodeUtf8 . percentDecode $ ipv4Text)
+  describe "pRegName" $ do
+    it "returns the matching reg name for all valid reg names" $ hedgehog $ do
+      regNameText <- forAll genValidRegName
+      let res = parseOnly (pRegName <* endOfInput) regNameText
+      res === (Right . TE.decodeUtf8 . percentDecode $ regNameText)
+  describe "pHost" $ do
+    it "returns the matching host for all valid hosts" $ hedgehog $ do
+      hostText <- forAll genValidHost
+      let res = parseOnly (pHost <* endOfInput) hostText
+      res === (Right . Host . TE.decodeUtf8 . percentDecode $ hostText)
+  describe "pPort" $ do
+    it "returns the matching host for all valid ports" $ hedgehog $ do
+      portText <- forAll genValidPort
+      let res = parseOnly (pPort <* endOfInput) portText
+      res === (Right . read . T.unpack . T.tail . TE.decodeUtf8 $ portText)
+  describe "pAuthority" $ do
+    it "generates an authority for all valid authorities" $ hedgehog $ do
+      authorityText <- forAll genValidAuthority
+      let res = parseOnly (pAuthority <* endOfInput) authorityText
+      isRight res === True
+  describe "pPathAbsAuth" $ do
+    it "generates a path for all valid absolute authority paths" $ hedgehog $ do
+      pathText <- forAll genValidPathAbsAuth
+      let res = parseOnly (pPathAbsAuth <* endOfInput) pathText
+      isRight res === True
+  describe "pPathAbsNoAuth" $ do
+    it "generates a path for all valid absolute no authority paths" $ hedgehog $ do
+      pathText <- forAll genValidPathAbsNoAuth
+      let res = parseOnly (pPathAbsNoAuth <* endOfInput) pathText
+      isRight res === True
+  describe "pPathRel" $ do
+    it "generates a path for all valid relative paths" $ hedgehog $ do
+      pathText <- forAll genValidPathRel
+      let res = parseOnly (pPathRel <* endOfInput) pathText
+      isRight res === True
+  describe "pQuery" $ do
+    it "returns the matching query for all valid queries" $ hedgehog $ do
+      queryText <- forAll genValidQuery
+      let res = parseOnly (pQuery <* endOfInput) queryText
+      res === (Right . Query . T.tail . TE.decodeUtf8 . percentDecode $ queryText)
+  describe "pFragment" $ do
+    it "returns the matching fragment for all valid fragments" $ hedgehog $ do
+      fragmentText <- forAll genValidFragment
+      let res = parseOnly (pFragment <* endOfInput) fragmentText
+      res === (Right . Fragment . T.tail . TE.decodeUtf8 . percentDecode $ fragmentText)
+  describe "pAbsoluteUri" $ do
+    it "generates an absolute uri for all valid absolute uris" $ hedgehog $ do
+      uriText <- forAll genValidAbsoluteUri
+      let res = parseOnly (pAbsoluteUri <* endOfInput) uriText
+      isRight res === True
+  describe "parseURI" $ do
+    it "generates uri components correctly for uri with scheme, host and path" $ do
+      let res = parseOnly pUri "http://google.com/test1/test2"
+      res `shouldBe` (Right uriWithHostAndPath)
+    it "generates uri components correctly for uri with upper case scheme, host and path" $ do
+      let res = parseOnly pUri "HTTP://google.com/test1/test2"
+      res `shouldBe` (Right uriWithHostAndPath)
+    it "generates uri components correctly for uri with host, username and path" $ do
+      let res = parseOnly pUri "http://j.t.kirk@google.com/test1/test2"
+      res `shouldBe` (Right uriWithHostUsernameAndPath)
+    it "generates uri components correctly for uri with host, username and path" $ do
+      let res = parseOnly pUri "http://j.t.kirk:11a@google.com/test1/test2"
+      res `shouldBe` (Right uriWithHostUsernamePasswordAndPath)
+    it "generates uri components correctly for uri with host, username, path and port" $ do
+      let res = parseOnly pUri "https://www.example.com:123/forum/questions/"
+      res `shouldBe` (Right uriWithHostPathAndPort)
+    it "generates uri components correctly for uri with host, username, path, port, query and fragment" $ do
+      let res = parseOnly pUri "https://www.example.com/forum/questions/?tag=networking&order=newest#top"
+      res `shouldBe` (Right uriWithHostPathQueryAndFragment)
+    it "generates uri components correctly when there is percent encoded unicode in the query" $ do
+      let res = parseOnly pUri "https://www.example.com/forum/questions/?tag=networking&order=newest%F0%9F%98%80"
+      res `shouldBe` (Right uriWithUnicodeInQuery)
+    it "generates uri components correctly when there are spaces in the query" $ do
+      let res = parseOnly pUri "https://www.example.com/forum/questions/?tag=with%20space"
+      res `shouldBe` (Right uriWithSpacesInQuery)
+    it "generates uri components correctly when there is percent encoded unicode in the fragment" $ do
+      let res = parseOnly pUri "https://www.example.com/forum/questions/#%F0%9F%98%80%F0%9F%98%80%F0%9F%98%80"
+      res `shouldBe` (Right uriWithUnicodeInFragment)
+    it "generates uri components correctly for ldap uri" $ do
+      let res = parseOnly pUri "ldap://192.168.0.1/c=GB?objectClass?one"
+      res `shouldBe` (Right ldapUri)
+    it "generates uri components correctly for tel uri" $ do
+      let res = parseOnly pUri "tel:+1-816-555-1212"
+      res `shouldBe` (Right telUri)
diff --git a/test/Dormouse/Uri/QQSpec.hs b/test/Dormouse/Uri/QQSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Dormouse/Uri/QQSpec.hs
@@ -0,0 +1,66 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ViewPatterns #-}
+
+module Dormouse.Uri.QQSpec 
+  ( spec
+  ) where
+
+import Test.Hspec
+import Dormouse.Uri.QQ
+import Dormouse.Url.QQ
+
+spec :: Spec
+spec = do
+  describe "http pattern" $ do
+    it "pattern matches correctly against a matching uri" $ do
+      let url' = [http|http://google.com|]
+      let matched = case url' of
+            [http|http://google.com|] -> True
+            _ -> False
+      matched `shouldBe` True
+    it "pattern match doesn't match a different uri" $ do
+      let url' = [http|http://google.com|]
+      let matched = case url' of
+            [http|http://test.com|] -> True
+            _ -> False
+      matched `shouldBe` False
+  describe "https pattern" $ do
+    it "pattern matches correctly against a matching uri" $ do
+      let url' = [https|https://google.com|]
+      let matched = case url' of
+            [https|https://google.com|] -> True
+            _ -> False
+      matched `shouldBe` True
+    it "pattern match doesn't match a different uri" $ do
+      let url' = [https|https://google.com|]
+      let matched = case url' of
+            [https|https://test.com|] -> True
+            _ -> False
+      matched `shouldBe` False
+  describe "uri pattern" $ do
+    it "pattern matches correctly against a matching uri" $ do
+      let uri' = [uri|https://google.com|]
+      let matched = case uri' of
+            [uri|https://google.com|] -> True
+            _ -> False
+      matched `shouldBe` True
+    it "pattern match doesn't match a different uri" $ do
+      let uri' = [uri|https://google.com|]
+      let matched = case uri' of
+            [uri|https://test.com|] -> True
+            _ -> False
+      matched `shouldBe` False
+  describe "relativeUri pattern" $ do
+    it "pattern matches correctly against a matching uri" $ do
+      let uri' = [uri|/myPath|]
+      let matched = case uri' of
+            [uri|/myPath|] -> True
+            _ -> False
+      matched `shouldBe` True
+    it "pattern match doesn't match a different uri" $ do
+      let uri' = [uri|/myPath2|]
+      let matched = case uri' of
+            [uri|/myPath|] -> True
+            _ -> False
+      matched `shouldBe` False
diff --git a/test/Dormouse/Uri/QuerySpec.hs b/test/Dormouse/Uri/QuerySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Dormouse/Uri/QuerySpec.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DisambiguateRecordFields #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE GADTs #-}
+
+module Dormouse.Uri.QuerySpec
+  ( spec
+  ) where
+
+import qualified Data.Text as T
+import Test.Hspec
+import Dormouse.Uri
+import Dormouse.Url
+import Dormouse.Url.Builder
+import Dormouse.Url.QQ
+
+uriWithDestructSequence :: Url "http"
+uriWithDestructSequence = HttpUrl $ UrlComponents
+  { urlAuthority = Authority {authorityUserInfo = Nothing, authorityHost = Host "enterprise.starfleet.com", authorityPort = Nothing}
+  , urlPath = Path ["destruct"]
+  , urlQuery = Just $ "code1=11a&code2=11a2b&code3=1b2b3&code4=000destruct-0"
+  , urlFragment = Nothing
+  }
+
+uriLookingUpTheEnterprise :: Url "http"
+uriLookingUpTheEnterprise = HttpUrl $ UrlComponents
+  { urlAuthority = Authority {authorityUserInfo = Nothing, authorityHost = Host "starfleet.com", authorityPort = Nothing}
+  , urlPath = Path ["ship"]
+  , urlQuery = Just $ "registry=1701"
+  , urlFragment = Nothing
+  }
+
+spec :: Spec
+spec = do
+  describe "build query" $ do
+    it "generates correct uri using the URI builder syntax with text components" $ do
+      let actualUri = [http|http://enterprise.starfleet.com|] </> "destruct" ? ("code1" =: ("11a" :: T.Text)) & ("code2" =: ("11a2b" :: T.Text)) & ("code3" =: ("1b2b3":: T.Text)) & ("code4" =: ("000destruct-0" :: T.Text))
+      actualUri `shouldBe` uriWithDestructSequence
+    it "generates correct uri using the URI builder syntax with int components" $ do
+      let actualUri = [http|http://starfleet.com|] </> "ship" ? ("registry" =: (1701 :: Int))
+      actualUri `shouldBe` uriLookingUpTheEnterprise
+
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
