Implement a more type safe API

This commit is contained in:
mrkkrp 2016-12-16 20:12:09 +03:00
parent ab508f1b4f
commit 212d0cb3e7
17 changed files with 287 additions and 226 deletions

View File

@ -39,7 +39,7 @@ script:
*) cabal test --show-details=always ;;
esac
- cabal sdist
- cabal haddock | grep "100%" | wc -l | grep "14"
- cabal haddock | grep "100%" | wc -l | grep "13"
# after_script:
# - cabal install hpc-coveralls

View File

@ -71,36 +71,26 @@ code or bug reports:
-}
module Text.Inflections
( camelize
, camelizeCustom
, dasherize
, humanize
, underscore
, titleize
, Transliterations
, defaultMap
, parameterize
, parameterizeCustom
, transliterate
, transliterateCustom
, ordinal
, ordinalize
, parseSnakeCase
, parseCamelCase
( camelize
, camelizeCustom
, dasherize
, humanize
, underscore
, titleize
, Transliterations
, defaultMap
, parameterize
, parameterizeCustom
, transliterate
, transliterateCustom
, ordinal
, ordinalize
, parseSnakeCase
, parseCamelCase
-- * Often used combinators
, toUnderscore
, toDashed
, toCamelCased
)
, toUnderscore
, toDashed
, toCamelCased )
where
import Control.Monad (liftM)
@ -114,9 +104,9 @@ import Text.Inflections.Ordinal (ordinal, ordinalize)
import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
import Text.Inflections.Parse.CamelCase (parseCamelCase)
import Text.Inflections.Parse.SnakeCase (parseSnakeCase)
import Text.Inflections.Parse.Types
import Text.Inflections.Titleize (titleize)
import Text.Inflections.Transliterate (transliterate, transliterateCustom)
import Text.Inflections.Types
import Text.Inflections.Underscore (underscore)
import Text.Megaparsec

View File

@ -18,34 +18,30 @@ module Text.Inflections.Camelize
where
import Data.Text (Text)
import Text.Inflections.Parse.Types
import Text.Inflections.Types
import qualified Data.Text as T
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#else
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
-- |Turns a an input Word List in into CamelCase. Returns the CamelCase String.
-- | Turn an input word list in into CamelCase.
--
-- >>> camelize [ Word "foo", Acronym "bar", Word "bazz" ]
-- "FoobarBazz"
camelize
:: [Word] -- ^ Input Words to separate with underscores
-> Text -- ^ The camelized 'Text'
:: [SomeWord] -- ^ Input words
-> Text -- ^ The camelized 'Text'
camelize = camelizeCustom True
{-# INLINE camelize #-}
-- |Turns an input Word List into a CamelCase String.
-- | Turn an input word list into a CamelCase String.
--
-- >>> camelizeCustom False [ Word "foo", Acronym "bar", Word "bazz" ]
-- "foobarBazz"
camelizeCustom
:: Bool -- ^ Whether to capitalize the first character in the output String
-> [Word] -- ^ The input Words
-> Text -- ^ The camelized 'Text'
:: Bool -- ^ Whether to capitalize the first character in the output String
-> [SomeWord] -- ^ The input Words
-> Text -- ^ The camelized 'Text'
camelizeCustom _ [] = ""
camelizeCustom c (x:xs) = T.concat $
mapWord (if c then T.toTitle else T.toLower) x : (mapWord T.toTitle <$> xs)
{-# INLINE camelizeCustom #-}
unSomeWord (if c then T.toTitle else T.toLower) x : (unSomeWord T.toTitle <$> xs)

View File

@ -9,25 +9,24 @@
--
-- Conversion to dasherized phrases.
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Inflections.Dasherize ( dasherize ) where
module Text.Inflections.Dasherize
( dasherize )
where
import Data.Text (Text)
import Text.Inflections.Parse.Types
import Text.Inflections.Types
import qualified Data.Text as T
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#endif
-- | Replaces underscores in a snake_cased string with dashes (hyphens).
--
-- >>> dasherize [ Word "foo", Acronym "bar", Word "bazz" ]
-- >>> foo <- SomeWord <$> mkWord "foo"
-- >>> bar <- SomeWord <$> mkAcronym "bar"
-- >>> bazz <- SomeWord <$> mkWord "bazz"
-- >>> dasherize [foo,bar,bazz]
-- "foo-bar-bazz"
dasherize
:: [Word] -- ^ Input Words to separate with dashes
-> Text -- ^ The dasherized String
dasherize = T.intercalate "-" . fmap (mapWord T.toLower)
{-# INLINE dasherize #-}
:: [SomeWord] -- ^ Input Words to separate with dashes
-> Text -- ^ The dasherized String
dasherize = T.intercalate "-" . fmap (unSomeWord T.toLower)

View File

@ -17,12 +17,10 @@ module Text.Inflections.Humanize
where
import Data.Text (Text)
import Text.Inflections.Parse.Types
import Text.Inflections.Types
import qualified Data.Text as T
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#else
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
@ -30,13 +28,15 @@ import Control.Applicative
-- 'Text.Inflections.Titleize.titleize', this is meant for creating pretty
-- output.
--
-- >>> humanize [ Word "foo", Acronym "bar", Word "bazz" ]
-- >>> humanize [Word "foo", Acronym "bar", Word "bazz"]
-- "Foo bar bazz"
--
-- Note that as of version 0.3.0.0 @Word@ and @Acronym@ constructors are
-- hidden, but you still can construct them with 'mkWord' and 'mkAcronym'.
humanize
:: [Word] -- ^ List of Words, first of which will be capitalized
-> Text -- ^ The humanized output
:: [SomeWord] -- ^ List of Words, first of which will be capitalized
-> Text -- ^ The humanized output
humanize xs' =
case mapWord (T.replace "_" " ") <$> xs' of
case unSomeWord (T.replace "_" " ") <$> xs' of
[] -> ""
(x:xs) -> T.unwords $ T.toTitle x : (T.toLower <$> xs)
{-# INLINE humanize #-}

View File

@ -1,32 +0,0 @@
-- |
-- Module : Text.Inflections.Parse.Acronym
-- Copyright : © 2016 Justin Leitgeb
-- License : MIT
--
-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
-- Stability : experimental
-- Portability : portable
--
-- Parser for acronyms.
{-# LANGUAGE CPP #-}
module Text.Inflections.Parse.Acronym
( acronym )
where
import Data.Text (Text)
import Text.Inflections.Parse.Types
import Text.Megaparsec
import Text.Megaparsec.Text
import qualified Data.Text as T
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#endif
-- | Parser that accepts a 'Text' from given collection and turns it into
-- an 'Acronym'.
acronym :: [Text] -> Parser Word
acronym = fmap (Acronym . T.pack) . choice . fmap (string . T.unpack)
{-# INLINE acronym #-}

View File

@ -10,26 +10,25 @@
-- Parser for camel case “symbols”.
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Inflections.Parse.CamelCase
( parseCamelCase )
where
import Control.Applicative
import Data.Text (Text)
import Text.Inflections.Parse.Acronym (acronym)
import Text.Inflections.Parse.Types (Word(..))
import Text.Inflections.Types
import Text.Megaparsec
import Text.Megaparsec.Text
import qualified Data.Text as T
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#else
import Control.Applicative
#endif
-- |Parse a CamelCase string.
-- | Parse a CamelCase string.
--
-- >>> parseCamelCase ["Bar"] "FooBarBazz"
-- Right [Word "Foo",Acronym "Bar",Word "Bazz"]
@ -37,20 +36,32 @@ import Control.Applicative
-- Left "(unknown)" (line 1, column 4):
-- unexpected '_'
parseCamelCase
:: [Text] -- ^ Collection of acronyms
-> Text -- ^ Input
-> Either (ParseError Char Dec) [Word] -- ^ Result of parsing
:: [Word 'Acronym] -- ^ Collection of acronyms
-> Text -- ^ Input
-> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
parseCamelCase acronyms = parse (parser acronyms) ""
parser
:: [Text] -- ^ Collection of acronyms
-> Parser [Word] -- ^ CamelCase parser
parser acronyms = many (acronym acronyms <|> word) <* eof
{-# INLINE parser #-}
:: [Word 'Acronym] -- ^ Collection of acronyms
-> Parser [SomeWord] -- ^ CamelCase parser
parser acronyms = many (a <|> n) <* eof
where
n = SomeWord <$> word
a = SomeWord <$> acronym acronyms
word :: Parser Word
acronym :: [Word 'Acronym] -> Parser (Word 'Acronym)
acronym acronyms = do
x <- T.pack <$> choice (string . T.unpack . unWord <$> acronyms)
case mkAcronym x of
Nothing -> empty -- cannot happen if the system is sound
Just acr -> return acr
{-# INLINE acronym #-}
word :: Parser (Word 'Normal)
word = do
firstChar <- upperChar <|> lowerChar
restChars <- many $ lowerChar <|> digitChar
return . Word . T.pack $ firstChar : restChars
case (mkWord . T.pack) (firstChar : restChars) of
Nothing -> empty -- cannot happen if the system is sound
Just wrd -> return wrd
{-# INLINE word #-}

View File

@ -9,47 +9,59 @@
--
-- Parser for snake case “symbols”.
{-# LANGUAGE CPP #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
module Text.Inflections.Parse.SnakeCase
( parseSnakeCase )
where
import Control.Applicative
import Data.Text (Text)
import Text.Inflections.Parse.Acronym (acronym)
import Text.Inflections.Parse.Types (Word(..))
import Text.Inflections.Types
import Text.Megaparsec
import Text.Megaparsec.Text
import qualified Data.Text as T
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#else
import Control.Applicative
#endif
-- |Parses a snake_case string.
-- | Parse a snake_case string.
--
-- >>> parseSnakeCase ["bar"] "foo_bar_bazz"
-- >>> bar <- mkAcronym "bar"
-- >>> parseSnakeCase [bar] "foo_bar_bazz"
-- Right [Word "foo",Acronym "bar",Word "bazz"]
--
-- >>> parseSnakeCase [] "fooBarBazz"
-- Left "(unknown)" (line 1, column 4):
-- unexpected 'B'
parseSnakeCase
:: [Text] -- ^ Collection of acronyms
:: [Word 'Acronym] -- ^ Collection of acronyms
-> Text -- ^ Input
-> Either (ParseError Char Dec) [Word] -- ^ Result of parsing
-> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
parseSnakeCase acronyms = parse (parser acronyms) ""
parser
:: [Text]
-> Parser [Word]
parser acronyms = do
ws <- (acronym acronyms <|> word) `sepBy` char '_'
eof
return ws
{-# INLINE parser #-}
:: [Word 'Acronym]
-> Parser [SomeWord]
parser acronyms = ((a <|> n) `sepBy` char '_') <* eof
where
n = SomeWord <$> word
a = SomeWord <$> acronym acronyms
word :: Parser Word
word = Word . T.pack <$> (some lowerChar <|> some digitChar)
acronym :: [Word 'Acronym] -> Parser (Word 'Acronym)
acronym acronyms = do
x <- T.pack <$> choice (string . T.unpack . unWord <$> acronyms)
case mkAcronym x of
Nothing -> empty -- cannot happen if the system is sound
Just acr -> return acr
{-# INLINE acronym #-}
word :: Parser (Word 'Normal)
word = do
x <- T.pack <$> (some lowerChar <|> some digitChar)
case mkWord x of
Nothing -> empty -- cannot happen if the system is sound
Just wrd -> return wrd
{-# INLINE word #-}

View File

@ -1,54 +0,0 @@
-- |
-- Module : Text.Inflections.Parse.Types
-- Copyright : © 2016 Justin Leitgeb
-- License : MIT
--
-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
-- Stability : experimental
-- Portability : portable
--
-- Types used in the library.
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Text.Inflections.Parse.Types
( Word (..) -- FIXME we should not export the constructor
, unWord
, mapWord
, InflectionException (..) )
where
import Control.Monad.Catch (Exception)
import Data.Text (Text)
import Data.Typeable (Typeable)
import Text.Megaparsec
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#endif
-- | A 'String' that should be kept whole through applied inflections
data Word
= Word Text -- ^ A word that may be transformed by inflection
| Acronym Text -- ^ A word that may not be transformed by inflections
deriving (Show, Eq)
-- | Get a 'Text' value from 'Word'.
unWord :: Word -> Text
unWord (Word s) = s
unWord (Acronym s) = s
{-# INLINE unWord #-}
-- | Apply 'Text' transforming function to a 'Word' unless it's a 'Acronym'.
mapWord :: (Text -> Text) -> Word -> Text
mapWord f (Word s) = f s
mapWord _ (Acronym s) = s
{-# INLINE mapWord #-}
-- | The exceptions that is thrown when parsing of input fails.
data InflectionException = InflectionParsingFailed (ParseError Char Dec)
deriving (Eq, Show, Typeable)
instance Exception InflectionException

View File

@ -9,26 +9,19 @@
--
-- Conversion to titleized phrases.
{-# LANGUAGE CPP #-}
module Text.Inflections.Titleize
( titleize )
where
import Data.Text (Text)
import Text.Inflections.Parse.Types
import Text.Inflections.Types
import qualified Data.Text as T
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#endif
-- | Capitalize all the Words in the input list.
--
-- >>> titleize [ Word "foo", Acronym "bar", Word "bazz" ]
-- "Foo bar Bazz"
titleize
:: [Word] -- ^ List of Words, first of which will be capitalized
-> Text -- ^ The titleized String
titleize = T.unwords . fmap (mapWord T.toTitle)
{-# INLINE titleize #-}
:: [SomeWord] -- ^ List of Words, first of which will be capitalized
-> Text -- ^ The titleized String
titleize = T.unwords . fmap (unSomeWord T.toTitle)

122
Text/Inflections/Types.hs Normal file
View File

@ -0,0 +1,122 @@
-- |
-- Module : Text.Inflections.Types
-- Copyright : © 2016 Justin Leitgeb
-- License : MIT
--
-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
-- Stability : experimental
-- Portability : portable
--
-- Types used in the library. Usually you don't need to import this module
-- and "Text.Inflections" should be enough.
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
module Text.Inflections.Types
( Word
, WordType (..)
, mkWord
, mkAcronym
, unWord
, SomeWord (..)
, unSomeWord
, InflectionException (..) )
where
import Control.Monad.Catch
import Data.Char (isSpace)
import Data.Data (Data)
import Data.Text (Text)
import Data.Typeable (Typeable)
import GHC.Generics (Generic)
import Text.Megaparsec
import qualified Data.Text as T
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#endif
-- | Create a word from given 'Text'. The input should not contain spaces or
-- 'InflectionInvalidWord' will be thrown.
mkWord :: MonadThrow m => Text -> m (Word 'Normal)
mkWord txt =
if T.any isSpace txt
then throwM (InflectionInvalidWord txt)
else return (Word txt)
-- | Create an acronym from given 'Text'. The input should not contain
-- spaces or 'InflectionInvalidAcronym' will be thrown. Acronym is different
-- from normal word by that it may not be transformed by inflections.
mkAcronym :: MonadThrow m => Text -> m (Word 'Acronym)
mkAcronym txt =
if T.any isSpace txt
then throwM (InflectionInvalidAcronym txt)
else return (Word txt)
-- | A 'Text' value that should be kept whole through applied inflections.
data Word (t :: WordType) = Word Text
deriving (Eq, Ord, Show)
instance Show (Word 'Normal) where
show (Word x) = "Word " ++ show x
instance Show (Word 'Acronym) where
show (Word x) = "Acronym " ++ show x
-- | A type-level tag for words.
data WordType = Normal | Acronym
-- | Get a 'Text' value from 'Word'.
unWord :: Word t -> Text
unWord (Word s) = s
-- | An existential wrapper that allows to keep words and acronyms in single
-- list for example. The only thing that receiver of 'SomeWord' can do is to
-- apply 'unWord' on it, of course. This is faciliated by 'unSomeWord'.
data SomeWord where
SomeWord :: Transformable (Word t) => Word t -> SomeWord
-- NOTE The constraint is only needed because GHC is not smart enough
-- (yet?) to figure out that t cannot be anything other than Normal and
-- Acronym and thus all cases are already covered by the instances
-- provided below.
instance Show SomeWord where
show (SomeWord w) = show w
-- | Extract 'Text' from 'SomeWord' and apply given function only if the
-- word inside wasn't an acronym.
unSomeWord :: (Text -> Text) -> SomeWord -> Text
unSomeWord f (SomeWord w) = transform f w
-- | Non public stuff.
class Transformable a where
transform :: (Text -> Text) -> a -> Text
instance Transformable (Word 'Normal) where
transform f = f . unWord
instance Transformable (Word 'Acronym) where
transform _ = unWord
-- | The exceptions that is thrown when parsing of input fails.
data InflectionException
= InflectionParsingFailed (ParseError Char Dec)
| InflectionInvalidWord Text
| InflectionInvalidAcronym Text
deriving (Eq, Show, Typeable, Data, Generic)
instance Exception InflectionException

View File

@ -9,7 +9,6 @@
--
-- Conversion to phrases separated by underscores.
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Inflections.Underscore
@ -17,19 +16,14 @@ module Text.Inflections.Underscore
where
import Data.Text (Text)
import Text.Inflections.Parse.Types
import Text.Inflections.Types
import qualified Data.Text as T
#if MIN_VERSION_base(4,8,0)
import Prelude hiding (Word)
#endif
-- |Turns a CamelCase string into an underscore_separated 'Text'.
--
-- >>> underscore [ Word "foo", Acronym "bar", Word "bazz" ]
-- "foo_bar_bazz"
underscore
:: [Word] -- ^ Input Words to separate with underscores
-> Text -- ^ The underscored String
underscore = T.intercalate "_" . fmap (mapWord T.toLower)
{-# INLINE underscore #-}
:: [SomeWord] -- ^ Input Words to separate with underscores
-> Text -- ^ The underscored String
underscore = T.intercalate "_" . fmap (unSomeWord T.toLower)

View File

@ -29,7 +29,7 @@ flag dev
library
exposed-modules: Text.Inflections
, Text.Inflections.Parse.Types
, Text.Inflections.Types
other-modules: Text.Inflections.Data
, Text.Inflections.Camelize
@ -37,7 +37,6 @@ library
, Text.Inflections.Humanize
, Text.Inflections.Ordinal
, Text.Inflections.Parameterize
, Text.Inflections.Parse.Acronym
, Text.Inflections.Parse.CamelCase
, Text.Inflections.Parse.SnakeCase
, Text.Inflections.Titleize

View File

@ -1,3 +1,4 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Inflections.DasherizeSpec
@ -7,9 +8,15 @@ where
import Test.Hspec
import Text.Inflections (dasherize)
import Text.Inflections.Parse.Types (Word (..))
import Text.Inflections.Types
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
spec :: Spec
spec = describe "dasherize" $
it "dasherizes a collection of words sentence" $
dasherize [Word "foo", Word "bar"] `shouldBe` "foo-bar"
it "dasherizes a collection of words sentence" $ do
foo <- SomeWord <$> mkWord "foo"
bar <- SomeWord <$> mkWord "bar"
dasherize [foo,bar] `shouldBe` "foo-bar"

View File

@ -1,3 +1,4 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Inflections.HumanizeSpec (spec) where
@ -5,13 +6,22 @@ module Text.Inflections.HumanizeSpec (spec) where
import Test.Hspec
import Text.Inflections (humanize)
import Text.Inflections.Parse.Types (Word(..))
import Text.Inflections.Types
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
spec :: Spec
spec = describe "humazine" $ do
it "converts snake case to a human-readable string" $
humanize [Word "employee", Word "salary"] `shouldBe` "Employee salary"
it "turns underscores into spaces" $
humanize [Word "employee", Word "has_salary"] `shouldBe` "Employee has salary"
it "capitalizes the first word of a sentence" $
humanize [Word "underground"] `shouldBe` "Underground"
it "converts snake case to a human-readable string" $ do
employee <- SomeWord <$> mkWord "employee"
salary <- SomeWord <$> mkWord "salary"
humanize [employee,salary] `shouldBe` "Employee salary"
it "turns underscores into spaces" $ do
employee <- SomeWord <$> mkWord "employee"
hasSalary <- SomeWord <$> mkWord "has_salary"
humanize [employee, hasSalary] `shouldBe` "Employee has salary"
it "capitalizes the first word of a sentence" $ do
underground <- SomeWord <$> mkWord "underground"
humanize [underground] `shouldBe` "Underground"

View File

@ -1,15 +1,22 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Inflections.TitleizeSpec (spec) where
import Test.Hspec
import Text.Inflections (titleize)
import Text.Inflections.Parse.Types (Word(..))
import Text.Inflections.Types
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
spec :: Spec
spec = describe "titleize" $ do
it "converts two words to title case" $
titleize [Word "Employee", Word "Salary"] `shouldBe` "Employee Salary"
it "converts one word to title case" $
titleize [Word "underground"] `shouldBe` "Underground"
it "converts two words to title case" $ do
employee <- SomeWord <$> mkWord "Employee"
salary <- SomeWord <$> mkWord "Salary"
titleize [employee,salary] `shouldBe` "Employee Salary"
it "converts one word to title case" $ do
underground <- SomeWord <$> mkWord "underground"
titleize [underground] `shouldBe` "Underground"

View File

@ -1,12 +1,19 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Inflections.UnderscoreSpec (spec) where
import Test.Hspec
import Text.Inflections (underscore)
import Text.Inflections.Parse.Types (Word(..))
import Text.Inflections.Types
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
spec :: Spec
spec = describe "underscore" $
it "converts a word list to snake case" $
underscore [Word "test", Word "this"] `shouldBe` "test_this"
it "converts a word list to snake case" $ do
test <- SomeWord <$> mkWord "test"
this <- SomeWord <$> mkWord "this"
underscore [test, this] `shouldBe` "test_this"