packages feed

slug 0.1.0 → 0.1.1

raw patch · 5 files changed

+152/−36 lines, 5 filesdep −HUnitdep −test-framework-hunitdep ~slugPVP ok

version bump matches the API change (PVP)

Dependencies removed: HUnit, test-framework-hunit

Dependency ranges changed: slug

API changes (from Hackage documentation)

+ Web.Slug: InvalidLength :: Int -> SlugException
+ Web.Slug: InvalidSlug :: Text -> SlugException
+ Web.Slug: instance GHC.Read.Read Web.Slug.Slug
+ Web.Slug: parseSlug :: MonadThrow m => Text -> m Slug
+ Web.Slug: truncateSlug :: MonadThrow m => Int -> Slug -> m Slug

Files

CHANGELOG.md view
@@ -1,3 +1,12 @@+## Slug 0.1.1++* Add `Read` instance of `Slug`.++* Add `parseSlug` and `truncateSlug` functions.++* Functions (including instance methods) that parse `Text` that must be+  formatted as valid slug are case-sensitive now.+ ## Slug 0.1.0  * Initial release.
README.md view
@@ -15,7 +15,7 @@ ## Quick start  The package provides data type `Slug` that is instance of various type-classes, so it can be used with Persistent or as part of route. It's also+classes, so it can be used with Persistent or as part of route. It also works with `aeson` package.  The slugs are completely type-safe. When you have a `Slug`, you can be sure
Web/Slug.hs view
@@ -10,16 +10,19 @@ -- Type-safe slug implementation for Yesod ecosystem.  {-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE TupleSections      #-}  module Web.Slug   ( Slug   , mkSlug   , unSlug+  , parseSlug+  , truncateSlug   , SlugException (..) ) where  import Control.Exception (Exception)-import Control.Monad (mzero, (>=>))+import Control.Monad (mzero, (>=>), liftM) import Control.Monad.Catch (MonadThrow (..)) import Data.Aeson.Types (ToJSON (..), FromJSON (..)) import Data.Char (isAlphaNum)@@ -35,10 +38,16 @@ -- | This exception is thrown by 'mkSlug' when its input cannot be converted -- into proper 'Slug'. -data SlugException = InvalidInput Text deriving (Typeable)+data SlugException+  = InvalidInput Text  -- ^ Slug cannot be generated for given text+  | InvalidSlug  Text  -- ^ Input is not a valid slug, see 'parseSlug'+  | InvalidLength Int  -- ^ Requested slug length is not a positive number+ deriving (Typeable)  instance Show SlugException where   show (InvalidInput text) = "Cannot build slug for " ++ show text+  show (InvalidSlug  text) = "The text is not a valid slug " ++ show text+  show (InvalidLength n)   = "Invalid slug length: " ++ show n  instance Exception SlugException @@ -56,14 +65,15 @@  newtype Slug = Slug   { unSlug :: Text     -- ^ Get textual representation of 'Slug'.-  } deriving (Eq, Show, Typeable)+  } deriving (Eq, Typeable)  -- | Create 'Slug' from 'Text', all necessary transformations are -- applied. Argument of this function can be title of an article or -- something like that. -- -- Note that result is inside 'MonadThrow', that means you can just get it--- in 'Maybe', in more complex contexts it will throw 'SlugException'.+-- in 'Maybe', in more complex contexts it will throw 'SlugException'+-- exception using 'InvalidInput' constructor. -- -- This function also have a useful property: --@@ -84,22 +94,56 @@ getSlugWords = T.words . T.toLower . T.map f . T.replace "'" ""   where f x = if isAlphaNum x then x else ' ' +-- | Convert 'Text' into 'Slug' only when it is already valid slug.+--+-- This function can throw 'SlugException' exception using 'InvalidSlug'+-- constructor.++parseSlug :: MonadThrow m => Text -> m Slug+parseSlug v = mkSlug v >>= check+  where check s =+          if unSlug s == v+          then return s+          else throwM (InvalidSlug v)++-- | Ensure that given 'Slug' is not longer than given maximum number of+-- characters. If truncated slug ends in a dash, remove that dash too. (Dash+-- at the end would violate properties described in documentation for+-- 'Slug'.)+--+-- If the first argument is not a positive number, 'SlugException' is thrown+-- using 'InvalidLength' constructor.++truncateSlug :: MonadThrow m+  => Int               -- ^ Maximum length of slug, must be greater than 0+  -> Slug              -- ^ Original non-truncated slug+  -> m Slug            -- ^ Truncated slug+truncateSlug n v+  | n < 1     = throwM (InvalidLength n)+  | otherwise = mkSlug . T.take n . unSlug $ v++instance Show Slug where+  show = show . unSlug++instance Read Slug where+  readsPrec n = (readsPrec n :: ReadS Text) >=> f+    where f (s, t) = (,t) `liftM` parseSlug s+ instance ToJSON Slug where   toJSON = toJSON . unSlug  instance FromJSON Slug where-  parseJSON (A.String v) = maybe mzero return (mkSlug v)+  parseJSON (A.String v) = maybe mzero return (parseSlug v)   parseJSON _            = mzero  instance PersistField Slug where   toPersistValue   = toPersistValue . unSlug   fromPersistValue =-    fromPersistValue >=> either (Left . T.pack . show) Right . mkSlug+    fromPersistValue >=> either (Left . T.pack . show) Right . parseSlug  instance PersistFieldSql Slug where   sqlType = const SqlString  instance PathPiece Slug where-  fromPathPiece v = mkSlug v >>= check-    where check s = if unSlug s == T.toLower v then Just s else Nothing-  toPathPiece = unSlug+  fromPathPiece = parseSlug+  toPathPiece   = unSlug
slug.cabal view
@@ -32,14 +32,14 @@ -- POSSIBILITY OF SUCH DAMAGE.  name:                 slug-version:              0.1.0+version:              0.1.1 cabal-version:        >= 1.10 license:              BSD3 license-file:         LICENSE.md author:               Mark Karpov <markkarpov@opmbx.org> maintainer:           Mark Karpov <markkarpov@opmbx.org>-homepage:             https://github.com/mrkkrp/htaglib-bug-reports:          https://github.com/mrkkrp/htaglib/issues+homepage:             https://github.com/mrkkrp/slug+bug-reports:          https://github.com/mrkkrp/slug/issues category:             Web synopsis:             Type-safe slugs for Yesod ecosystem build-type:           Simple@@ -77,12 +77,11 @@     ghc-options:      -O2 -Wall   default-extensions: OverloadedStrings   build-depends:      QuickCheck                 >= 2.4 && < 3-                    , HUnit                      >= 1.2 && < 1.4                     , base                       >= 4.6 && < 5+                    , exceptions                 >= 0.6                     , path-pieces                >= 0.1.5-                    , slug                       >= 0.1+                    , slug                       >= 0.1.1                     , test-framework             >= 0.6 && < 1-                    , test-framework-hunit       >= 0.2 && < 0.4                     , test-framework-quickcheck2 >= 0.3 && < 0.4                     , text                       >= 1.0   default-language:   Haskell2010
tests/Main.hs view
@@ -37,13 +37,13 @@ module Main (main) where  import Control.Monad ((>=>))-import Data.Char (isAlphaNum)-import Data.Maybe (isJust)+import Control.Monad.Catch (MonadThrow (..))+import Data.Char (isAlphaNum, isUpper)+import Data.Function (on)+import Data.Maybe (fromJust, isJust, isNothing) import Data.Text (Text) import Test.Framework-import Test.Framework.Providers.HUnit (testCase) import Test.Framework.Providers.QuickCheck2 (testProperty)-import Test.HUnit hiding (Test, path) import Test.QuickCheck import Web.PathPieces import Web.Slug@@ -58,23 +58,96 @@  tests :: Test tests = testGroup "Slug properties"-  [ testProperty "Slug of slug is the slug" prop_changeStops-  , testProperty "Alpha-numeric chars are necessary and sufficient"-    prop_needAlphaNum+  [ -- Properties of valid slugs+    testProperty "Slug cannot be empty" prop_nonEmpty+  , testProperty "Only dashes and alpha-numeric" prop_validContent+  , testProperty "Slug cannot begin with a dash" prop_noBegDash+  , testProperty "Slug cannot end with a dash" prop_noEndDash+  , testProperty "Non-empty word between dashes" prop_noEmptyWords+  , testProperty "No upper-cased chars in slugs" prop_noUpperCase+    -- Properties of helper-functions+  , testProperty "Slug of slug is the slug" prop_changeStops+  , testProperty "Parsing of slugs (success)" prop_parsing0+  , testProperty "Parsing of slugs (failure)" prop_parsing1+  , testProperty "Slug truncation" prop_truncation+  , testProperty "Reading of slugs (success)" prop_read0+  , testProperty "Reading of slugs (failure)" prop_read1+    -- Additional properties+  , testProperty "Rendering of slugs" prop_showSlug+  , testProperty "Alpha-numerics are necessary and sufficient" prop_needAlphaNum   , testProperty "Path pieces accept correct slugs" prop_pathPiece-  , testCase "Removal of apostrophe" prop_apostropheRemoval-  , testCase "Path pieces ignore case of slugs" prop_pathPieceCase   ]  instance Arbitrary Text where   arbitrary = T.pack <$> arbitrary +instance Arbitrary Slug where+  arbitrary = fromJust <$> ((mkSlug <$> arbitrary) `suchThat` isJust)++----------------------------------------------------------------------------+-- Properties of valid slugs++prop_nonEmpty :: Slug -> Property+prop_nonEmpty = expectFailure . T.null . unSlug++prop_validContent :: Slug -> Property+prop_validContent = property . T.all f . unSlug+  where f x = isAlphaNum x || x == '-'++prop_noBegDash :: Slug -> Property+prop_noBegDash s = expectFailure $ T.head (unSlug s) === '-'++prop_noEndDash :: Slug -> Property+prop_noEndDash s = expectFailure $ T.last (unSlug s) === '-'++prop_noEmptyWords :: Slug -> Property+prop_noEmptyWords = expectFailure . any T.null . T.splitOn "-" . unSlug++prop_noUpperCase :: Slug -> Property+prop_noUpperCase = expectFailure . T.any isUpper . unSlug++----------------------------------------------------------------------------+-- Properties of helper-functions++infix 4 ====++(====) :: (Show a, Show b, Eq b) => Either a b -> Either a b -> Property+(====) = (===) `on` displayLeft++displayLeft :: Show a => Either a b -> Either String b+displayLeft = either (Left . show) Right+ prop_changeStops :: Text -> Property-prop_changeStops x = v (f x) === v (g x)+prop_changeStops x = f x ==== g x   where f = mkSlug         g = mkSlug >=> mkSlug . unSlug-        v = either (Left . show) Right +prop_parsing0 :: Slug -> Property+prop_parsing0 s = parseSlug (unSlug s) === Just s++prop_parsing1 :: Text -> Property+prop_parsing1 x = (unSlug <$> mkSlug x) `notElem` [Nothing, Just x]+  ==> parseSlug x ==== throwM (InvalidSlug x)++prop_truncation :: Int -> Slug -> Property+prop_truncation n s =+  case truncateSlug n s of+    Left e -> n < 1 ==> show e === show (InvalidLength n)+    Right t -> n > 0 ==> T.length (unSlug t) <= n++prop_read0 :: Slug -> Property+prop_read0 s = read (show s) === s++prop_read1 :: Text -> Property+prop_read1 s = isNothing (parseSlug s)+  ==> (reads (show s) :: [(Slug, String)]) === []++----------------------------------------------------------------------------+-- Additional properties++prop_showSlug :: Slug -> Property+prop_showSlug s = show s === show (unSlug s)+ prop_needAlphaNum :: Text -> Property prop_needAlphaNum x = hasAlphaNum x ==> isJust (mkSlug x)   where hasAlphaNum = isJust . T.find isAlphaNum@@ -84,12 +157,3 @@   case mkSlug x of     Nothing -> property True     Just slug -> fromPathPiece (unSlug slug) === Just slug--prop_apostropheRemoval :: Assertion-prop_apostropheRemoval = unSlug <$> mkSlug text @?= Just slug-  where text = "That's what I thought about doin' that stuff!"-        slug = "thats-what-i-thought-about-doin-that-stuff"--prop_pathPieceCase :: Assertion-prop_pathPieceCase = fromPathPiece text @?= mkSlug text-  where text = "thiS-Строка-hAs-коМбиНацию-of-DiffErent-Cases" :: Text