git.iko.soy/typeable/inflections-hs
on master help sign in
Merge pull request #43 from stackbuilders/hide-word-constructors

Implement a more type safe API

commitc964f2ad153163e92b6f3c30e914baab5c68aed3 authorMark Karpov <markkarpov@opmbx.org> dateWed 28 Dec 2016, 14:32 (10 years ago) parentab508f12348bb7
27 files changed+675−378
.travis.yml+1−1 CHANGELOG.md+21−5 Text/Inflections.hs+143−125 Text/Inflections/Camelize.hs+18−16 Text/Inflections/Dasherize.hs+12−13 Text/Inflections/Data.hs+11−5 Text/Inflections/Humanize.hs+10−10 Text/Inflections/Ordinal.hs+14−15 Text/Inflections/Parameterize.hs+4−4 Text/Inflections/Parse/Acronym.hs−32 Text/Inflections/Parse/CamelCase.hs+36−17 Text/Inflections/Parse/SnakeCase.hs+28−21 Text/Inflections/Parse/Types.hs−54 Text/Inflections/Titleize.hs+8−12 Text/Inflections/Transliterate.hs+3−4 Text/Inflections/Types.hs+143 Text/Inflections/Underscore.hs+9−12 inflections.cabal+6−3 stack.yaml+3 test/Text/Inflections/DasherizeSpec.hs+10−4 test/Text/Inflections/HumanizeSpec.hs+18−8 test/Text/Inflections/Parse/CamelCaseSpec.hs+47 test/Text/Inflections/Parse/SnakeCaseSpec.hs+35 test/Text/Inflections/TitleizeSpec.hs+12−6 test/Text/Inflections/TypesSpec.hs+51 test/Text/Inflections/UnderscoreSpec.hs+10−4 test/Text/InflectionsSpec.hs+22−7
.travis.yml+1−1
@@ -39,7 +39,7 @@ script:
3939 *) cabal test --show-details=always ;;
4040 esac
4141 - cabal sdist
42− - cabal haddock | grep "100%" | wc -l | grep "14"
42+ - cabal haddock | grep "100%" | wc -l | grep "13"
4343
4444# after_script:
4545# - cabal install hpc-coveralls
CHANGELOG.md+21−5
@@ -1,16 +1,32 @@
11## Inflections 0.3.0.0
22
3−* Added the `CHANGELOG.md` file.
3+* A more type-safe API forbidding creation of invalid words.
44
5−* Switched test suite to Hspec.
5+* Made the API use `Text` instead of `String` (which significally improved
6+ speed).
67
78* Switched to Megaparsec 5 for parsing.
89
9−* Made the API use `Text` instead of `String` (which significally improved
10− speed).
10+* Renamed `defaultMap` to `defaultTransliterations`.
11+
12+* Words now can contain digits (recognized by all parsers).
13+
14+* `parseSnakeCase` now is not confused when a word happens to have prefix
15+ coinciding with an acronym. This is harder to fix for `parseCamelCase`
16+ because acronym may contain capital letters, so old behavior is preserved
17+ for `parseCamelCase` for now.
18+
19+* `parseCamelCase` and `parseSnakeCase` take any instance of `Foldable` as a
20+ collection of acronyms, not just lists.
21+
22+* Added the `CHANGELOG.md` file.
23+
24+* Switched test suite to Hspec.
1125
1226* The `toUnderscore`, `toDashed`, and `toCamelCased` are not partial
13− anymore, now they operate in `MonadThrow`.
27+ anymore. They return parse error in `Left` just like parsing functions,
28+ but this result can be lifted into any instance of `MonadThrow` with
29+ `betterThrow` helper.
1430
1531* Improved documentation.
1632
Text/Inflections.hs+143−125
@@ -1,162 +1,180 @@
1−{- |
2−Module : Text.Inflections
3−Description : Rails-like inflections library for common String transformations.
4−Copyright : (c) Justin Leitgeb
5−License : MIT
6−
7−Maintainer : justin@stackbuilders.com
8−Stability : unstable
9−Portability : portable
10−
11−This module provides methods for common String transformations, similar to the
12−"Inflections" library found in Rails:
13−
14−<http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
15−
16−While many of the functions in this library are the same as in implementations
17−in Rails' ActiveSupport, the philosophy of this library is fundamentally
18−different. Where Rails tries to be as permissive as possible, and return a
19−String when given any input, this library tries to output strings that make
20−sense according to the function that is called.
21−
22−When you look closely at many of the functions in Rails' inflections
23−library, you will notice that many of them are partial. That is, they only
24−have well-defined output for some of the possible inputs to the function allowed
25−by the type system. As an example, let's take the @underscore@ function. In
26−Rails, it works like this:
27−
28−>>> "fooBar".underscore
29−"foo_bar"
30−
31−Looks ok so far. However, it's also easy to produce less expected results:
32−
33−>>> "foo bar".underscore
34−"foo bar"
35−
36−The output isn't underscored - it contains a space! It turns out that some of
37−the functions from Inflections in ActiveSupport are /partial/. Ie., the outputs
38−are really only specified for a certain range of the inputs allowed by the
39−String type.
40−
41−In the Haskell inflections library, we aim to deliver more predictable results
42−by separating the parsing of strings into tokens from the application of
43−transformations. Let's see an example.
44−
45−First, we tokenize an underscored String using 'parseSnakeCase':
46−
47−>>> parseSnakeCase [] "foo_bar"
48−Right [Word "foo",Word "bar"]
49−
50−We can chain together the tokenization of the input String and the
51−transformation to CamelCase by using 'Control.Monad.LiftM':
52−
53−>>> import Control.Monad (liftM)
54−>>> liftM camelize $ parseSnakeCase "foo_bar"
55−
56−By separating out the tokenization from the application of inflections, we also
57−end up with useful libraries for validating input which can be used
58−independently:
59−
60−>>> parseSnakeCase [] "fooBar"
61−Left "(unknown)" (line 1, column 4):
62−unexpected 'B'
63−expecting lowercase letter, "_" or end of input
64−
65−This library is still a work-in-progress, and contributions are welcome for
66−missing pieces and to fix bugs. Please see the Github page to contribute with
67−code or bug reports:
68−
69−<https://github.com/stackbuilders/inflections-hs>
1+-- |
2+-- Module : Text.Inflections
3+-- Description : Rails-like inflections library for common String transformations.
4+-- Copyright : (c) Justin Leitgeb
5+-- License : MIT
6+--
7+-- Maintainer : justin@stackbuilders.com
8+-- Stability : unstable
9+-- Portability : portable
10+--
11+-- This module provides methods for common String transformations, similar
12+-- to the Inflections library found in Rails:
13+--
14+-- <http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
15+--
16+-- While many of the functions in this library are the same as in
17+-- implementations in Rails' ActiveSupport, the philosophy of this library
18+-- is fundamentally different. Where Rails tries to be as permissive as
19+-- possible, and return a String when given any input, this library tries to
20+-- output strings that make sense according to the function that is called.
21+--
22+-- When you look closely at many of the functions in Rails' inflections
23+-- library, you will notice that many of them are partial. That is, they
24+-- only have well-defined output for some of the possible inputs to the
25+-- function allowed by the type system. As an example, let's take the
26+-- @underscore@ function. In Rails, it works like this:
27+--
28+-- >>> "fooBar".underscore
29+-- "foo_bar"
30+--
31+-- Looks OK so far. However, it's also easy to produce less expected results:
32+--
33+-- >>> "foo bar".underscore
34+-- "foo bar"
35+--
36+-- The output isn't underscored - it contains a space! It turns out that
37+-- some of the functions from Inflections in ActiveSupport are /partial/.
38+-- I.e., the outputs are really only specified for a certain range of the
39+-- inputs allowed by the String type.
40+--
41+-- In the Haskell inflections library, we aim to deliver more predictable
42+-- results by separating the parsing of strings into tokens from the
43+-- application of transformations. Let's see an example.
44+--
45+-- First, we tokenize an underscored String using 'parseSnakeCase':
46+--
47+-- >>> parseSnakeCase [] "foo_bar"
48+-- Right [Word "foo",Word "bar"]
49+--
50+-- We can chain together the tokenization of the input String and the
51+-- transformation to CamelCase by using 'fmap':
52+--
53+-- >>> camelize <$> parseSnakeCase [] "foo_bar"
54+-- Right "FooBar"
55+--
56+-- By separating out the tokenization from the application of inflections,
57+-- we also end up with useful libraries for validating input which can be
58+-- used independently:
59+--
60+-- >>> parseSnakeCase [] "fooBar"
61+-- 1:4:
62+-- unexpected 'B'
63+-- expecting '_', end of input, or lowercase letter
64+--
65+-- As of version 0.3.0.0, we don't permit creation of invalid 'Word's by
66+-- using of the smart constructors 'mkWord' and 'mkAcronym'. This is done
67+-- because not every 'Text' value is a valid 'Word', as it should not
68+-- contain whitespace, for example. Normal words have the type @'Word'
69+-- 'Normal'@, while acronyms have the type @'Word' 'Acronym'@. If you need
70+-- to have several words\/acronyms in a single list, use the existential
71+-- wrapper 'SomeWord'. Parsing functions now produce 'SomeWord's.
72+--
73+-- This library is still a work-in-progress, and contributions are welcome
74+-- for missing pieces and to fix bugs. Please see the Github page to
75+-- contribute with code or bug reports:
76+--
77+-- <https://github.com/stackbuilders/inflections-hs>
7078
71−-}
79+{-# LANGUAGE CPP #-}
7280
7381module Text.Inflections
74− ( camelize
75− , camelizeCustom
76−
77− , dasherize
78−
79− , humanize
80−
81− , underscore
82−
83− , titleize
84−
85− , Transliterations
86− , defaultMap
87−
88− , parameterize
89− , parameterizeCustom
90−
91− , transliterate
92− , transliterateCustom
93−
94− , ordinal
95− , ordinalize
96−
97− , parseSnakeCase
98− , parseCamelCase
82+ ( -- * Parsing
83+ parseSnakeCase
84+ , parseCamelCase
85+ -- * Rendering
86+ , camelize
87+ , camelizeCustom
88+ , dasherize
89+ , humanize
90+ , underscore
91+ , titleize
92+ , Transliterations
93+ , defaultTransliterations
94+ , parameterize
95+ , parameterizeCustom
96+ , transliterate
97+ , transliterateCustom
98+ , ordinalize
99+ , ordinal
100+ -- * Types and helpers
101+ , Word
102+ , WordType (..)
103+ , mkWord
104+ , mkAcronym
105+ , unWord
106+ , SomeWord (..)
107+ , unSomeWord
108+ , InflectionException (..)
99109 -- * Often used combinators
100− , toUnderscore
101− , toDashed
102− , toCamelCased
103− )
110+ , toUnderscore
111+ , toDashed
112+ , toCamelCased
113+ , betterThrow )
104114where
105115
106−import Control.Monad (liftM)
107116import Control.Monad.Catch (MonadThrow (..))
108117import Data.Text (Text)
109118import Text.Inflections.Camelize (camelize, camelizeCustom)
110119import Text.Inflections.Dasherize (dasherize)
111−import Text.Inflections.Data (Transliterations, defaultMap)
120+import Text.Inflections.Data (Transliterations, defaultTransliterations)
112121import Text.Inflections.Humanize (humanize)
113122import Text.Inflections.Ordinal (ordinal, ordinalize)
114123import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
115124import Text.Inflections.Parse.CamelCase (parseCamelCase)
116125import Text.Inflections.Parse.SnakeCase (parseSnakeCase)
117−import Text.Inflections.Parse.Types
118126import Text.Inflections.Titleize (titleize)
119127import Text.Inflections.Transliterate (transliterate, transliterateCustom)
128+import Text.Inflections.Types
120129import Text.Inflections.Underscore (underscore)
121130import Text.Megaparsec
122131
123−-- | Transforms CamelCasedString to snake_cased_string_with_underscores. In
124−-- case of failed parsing 'InflectionException' is thrown.
132+#if MIN_VERSION_base(4,8,0)
133+import Prelude hiding (Word)
134+#endif
135+
136+-- | Transforms CamelCasedString to snake_cased_string_with_underscores.
137+--
138+-- > toUnderscore = fmap underscore . parseCamelCase []
125139--
126140-- >>> toUnderscore "FooBarBazz"
127141-- "foo_bar_bazz"
128−toUnderscore :: MonadThrow m => Text -> m Text
129−toUnderscore = liftM underscore . handleEither . parseCamelCase []
130−{-# INLINE toUnderscore #-}
142+toUnderscore :: Text -> Either (ParseError Char Dec) Text
143+toUnderscore = fmap underscore . parseCamelCase []
131144
132−-- | Transforms CamelCasedString to snake-cased-string-with-dashes. In case
133−-- of failed parsing 'InflectionException' is thrown.
145+-- | Transforms CamelCasedString to snake-cased-string-with-dashes.
146+--
147+-- > toDashed = fmap dasherize . parseCamelCase []
134148--
135149-- >>> toDashed "FooBarBazz"
136150-- "foo-bar-bazz"
137−toDashed :: MonadThrow m => Text -> m Text
138−toDashed = liftM dasherize . handleEither . parseCamelCase []
139−{-# INLINE toDashed #-}
151+toDashed :: Text -> Either (ParseError Char Dec) Text
152+toDashed = fmap dasherize . parseCamelCase []
140153
141154-- | Transforms underscored_text to CamelCasedText. If first argument is
142155-- 'True' then FirstCharacter in result string will be in upper case. If
143−-- 'False' then firstCharacter will be in lower case. In case of failed
144−-- parsing 'InflectionException' is thrown.
156+-- 'False' then firstCharacter will be in lower case.
157+--
158+-- > toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
145159--
146160-- >>> toCamelCased True "foo_bar_bazz"
147161-- "FooBarBazz"
148162-- >>> toCamelCased False "foo_bar_bazz"
149163-- "fooBarBazz"
150−toCamelCased :: MonadThrow m
151− => Bool -- ^ Capitalize the first character
164+toCamelCased
165+ :: Bool -- ^ Capitalize the first character
152166 -> Text -- ^ Input
153− -> m Text -- ^ Ouput
154−toCamelCased t = liftM (camelizeCustom t) . handleEither . parseSnakeCase []
155−{-# INLINE toCamelCased #-}
167+ -> Either (ParseError Char Dec) Text -- ^ Ouput
168+toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
169+
170+-- | Lift something of type @'Either' ('ParseError' 'Char' 'Dec') a@ to
171+-- an instance of 'MonadThrow'. Useful when you want to shortcut on parsing
172+-- failures and you're in an instance of 'MonadThrow'.
173+--
174+-- This throws 'InflectionParsingFailed' if given value is inside 'Left'.
175+--
176+-- /since 0.3.0.0/
156177
157−-- | Take an 'Either' that can contain a parser error and throw it if
158−-- necessary. If everything is OK, just return 'Right' value.
159−handleEither :: MonadThrow m => Either (ParseError Char Dec) a -> m a
160−handleEither (Left err) = throwM (InflectionParsingFailed err)
161−handleEither (Right x) = return x
162−{-# INLINE handleEither #-}
178+betterThrow :: MonadThrow m => Either (ParseError Char Dec) a -> m a
179+betterThrow (Left err) = throwM (InflectionParsingFailed err)
180+betterThrow (Right x) = return x
Text/Inflections/Camelize.hs+18−16
@@ -18,34 +18,36 @@ module Text.Inflections.Camelize
1818where
1919
2020import Data.Text (Text)
21−import Text.Inflections.Parse.Types
21+import Text.Inflections.Types
2222import qualified Data.Text as T
2323
24−#if MIN_VERSION_base(4,8,0)
25−import Prelude hiding (Word)
26−#else
24+#if !MIN_VERSION_base(4,8,0)
2725import Control.Applicative
2826#endif
2927
30−-- |Turns a an input Word List in into CamelCase. Returns the CamelCase String.
28+-- | Turn an input word list in into CamelCase.
3129--
32−-- >>> camelize [ Word "foo", Acronym "bar", Word "bazz" ]
30+-- >>> foo <- SomeWord <$> mkWord "foo"
31+-- >>> bar <- SomeWord <$> mkAcronym "bar"
32+-- >>> bazz <- SomeWord <$> mkAcronym "bazz"
33+-- >>> camelize [foo,bar,bazz]
3334-- "FoobarBazz"
3435camelize
35− :: [Word] -- ^ Input Words to separate with underscores
36− -> Text -- ^ The camelized 'Text'
36+ :: [SomeWord] -- ^ Input words
37+ -> Text -- ^ The camelized 'Text'
3738camelize = camelizeCustom True
38−{-# INLINE camelize #-}
3939
40−-- |Turns an input Word List into a CamelCase String.
40+-- | Turn an input word list into a CamelCase String.
4141--
42−-- >>> camelizeCustom False [ Word "foo", Acronym "bar", Word "bazz" ]
42+-- >>> foo <- SomeWord <$> mkWord "foo"
43+-- >>> bar <- SomeWord <$> mkAcronym "bar"
44+-- >>> bazz <- SomeWord <$> mkAcronym "bazz"
45+-- >>> camelizeCustom False [foo,bar,bazz]
4346-- "foobarBazz"
4447camelizeCustom
45− :: Bool -- ^ Whether to capitalize the first character in the output String
46− -> [Word] -- ^ The input Words
47− -> Text -- ^ The camelized 'Text'
48+ :: Bool -- ^ Whether to capitalize the first character in the output String
49+ -> [SomeWord] -- ^ The input Words
50+ -> Text -- ^ The camelized 'Text'
4851camelizeCustom _ [] = ""
4952camelizeCustom c (x:xs) = T.concat $
50− mapWord (if c then T.toTitle else T.toLower) x : (mapWord T.toTitle <$> xs)
51−{-# INLINE camelizeCustom #-}
53+ unSomeWord (if c then T.toTitle else T.toLower) x : (unSomeWord T.toTitle <$> xs)
Text/Inflections/Dasherize.hs+12−13
@@ -9,25 +9,24 @@
99--
1010-- Conversion to dasherized phrases.
1111
12−{-# LANGUAGE CPP #-}
1312{-# LANGUAGE OverloadedStrings #-}
1413
15−module Text.Inflections.Dasherize ( dasherize ) where
14+module Text.Inflections.Dasherize
15+ ( dasherize )
16+where
1617
1718import Data.Text (Text)
18−import Text.Inflections.Parse.Types
19+import Text.Inflections.Types
1920import qualified Data.Text as T
2021
21−#if MIN_VERSION_base(4,8,0)
22−import Prelude hiding (Word)
23−#endif
24−
25−-- | Replaces underscores in a snake_cased string with dashes (hyphens).
22+-- | Produce a string with words separated by dashes (hyphens).
2623--
27−-- >>> dasherize [ Word "foo", Acronym "bar", Word "bazz" ]
24+-- >>> foo <- SomeWord <$> mkWord "foo"
25+-- >>> bar <- SomeWord <$> mkAcronym "bar"
26+-- >>> bazz <- SomeWord <$> mkWord "bazz"
27+-- >>> dasherize [foo,bar,bazz]
2828-- "foo-bar-bazz"
2929dasherize
30− :: [Word] -- ^ Input Words to separate with dashes
31− -> Text -- ^ The dasherized String
32−dasherize = T.intercalate "-" . fmap (mapWord T.toLower)
33−{-# INLINE dasherize #-}
30+ :: [SomeWord] -- ^ Input words to separate with dashes
31+ -> Text -- ^ The dasherized 'Text'
32+dasherize = T.intercalate "-" . fmap (unSomeWord T.toLower)
Text/Inflections/Data.hs+11−5
@@ -9,19 +9,25 @@
99--
1010-- Auxiliary data used in the library.
1111
12−module Text.Inflections.Data where
12+module Text.Inflections.Data
13+ ( Transliterations
14+ , defaultTransliterations )
15+where
1316
1417import Data.HashMap.Strict (HashMap)
1518import qualified Data.HashMap.Strict as M
1619
17−-- |A 'Data.Map.Map' containing mappings from international characters to
20+-- | A 'HashMap' containing mappings from international characters to
1821-- sequences approximating these characters within the ASCII range.
1922type Transliterations = HashMap Char String
2023
21−-- |These default transliterations stolen from the Ruby i18n library - see
24+-- | These default transliterations are stolen from the Ruby i18n library -
25+-- see
2226-- <https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/transliterator.rb#L41:L69>.
23−defaultMap :: Transliterations
24−defaultMap = M.fromList [
27+--
28+-- NOTE: before version 0.3.0.0 this was called @defaultMap@.
29+defaultTransliterations :: Transliterations
30+defaultTransliterations = M.fromList [
2531 ('À', "A"), ('Á', "A"), ('Â', "A"), ('Ã', "A"), ('Ä', "A"), ('Å', "A"),
2632 ('Æ', "AE"), ('Ç', "C"), ('È', "E"), ('É', "E"), ('Ê', "E"), ('Ë', "E"),
2733 ('Ì', "I"), ('Í', "I"), ('Î', "I"), ('Ï', "I"), ('Ð', "D"), ('Ñ', "N"),
Text/Inflections/Humanize.hs+10−10
@@ -17,26 +17,26 @@ module Text.Inflections.Humanize
1717where
1818
1919import Data.Text (Text)
20−import Text.Inflections.Parse.Types
20+import Text.Inflections.Types
2121import qualified Data.Text as T
2222
23−#if MIN_VERSION_base(4,8,0)
24−import Prelude hiding (Word)
25−#else
23+#if !MIN_VERSION_base(4,8,0)
2624import Control.Applicative
2725#endif
2826
29−-- |Capitalizes the first word and turns underscores into spaces. Like
27+-- | Capitalize the first word and separate words with spaces. Like
3028-- 'Text.Inflections.Titleize.titleize', this is meant for creating pretty
3129-- output.
3230--
33−-- >>> humanize [ Word "foo", Acronym "bar", Word "bazz" ]
31+-- >>> foo <- SomeWord <$> mkWord "foo"
32+-- >>> bar <- SomeWord <$> mkAcronym "bar"
33+-- >>> bazz <- SomeWord <$> mkWord "bazz"
34+-- >>> humanize [foo,bar,bazz]
3435-- "Foo bar bazz"
3536humanize
36− :: [Word] -- ^ List of Words, first of which will be capitalized
37− -> Text -- ^ The humanized output
37+ :: [SomeWord] -- ^ List of words, first of which will be capitalized
38+ -> Text -- ^ The humanized output
3839humanize xs' =
39− case mapWord (T.replace "_" " ") <$> xs' of
40+ case unSomeWord (T.replace "_" " ") <$> xs' of
4041 [] -> ""
4142 (x:xs) -> T.unwords $ T.toTitle x : (T.toLower <$> xs)
42−{-# INLINE humanize #-}
Text/Inflections/Ordinal.hs+14−15
@@ -12,14 +12,26 @@
1212{-# LANGUAGE OverloadedStrings #-}
1313
1414module Text.Inflections.Ordinal
15− ( ordinal
16− , ordinalize )
15+ ( ordinalize
16+ , ordinal )
1717where
1818
1919import Data.Monoid ((<>))
2020import Data.Text (Text)
2121import qualified Data.Text as T
2222
23+-- |Turns a number into an ordinal string used to denote the position in an
24+-- ordered sequence such as 1st, 2nd, 3rd, 4th.
25+--
26+-- >>> ordinalize 1
27+-- "1st"
28+-- >>> ordinalize 2
29+-- "2nd"
30+-- >>> ordinalize 10
31+-- "10th"
32+ordinalize :: (Integral a, Show a) => a -> Text
33+ordinalize n = T.pack (show n) <> ordinal n
34+
2335-- |Returns the suffix that should be added to a number to denote the position
2436-- in an ordered sequence such as 1st, 2nd, 3rd, 4th.
2537--
@@ -39,16 +51,3 @@ ordinal number
3951 where abs_number = abs number
4052 remainder10 = abs_number `mod` 10
4153 remainder100 = abs_number `mod` 100
42−
43−-- |Turns a number into an ordinal string used to denote the position in an
44−-- ordered sequence such as 1st, 2nd, 3rd, 4th.
45−--
46−-- >>> ordinalize 1
47−-- "1st"
48−-- >>> ordinalize 2
49−-- "2nd"
50−-- >>> ordinalize 10
51−-- "10th"
52−ordinalize :: (Integral a, Show a) => a -> Text
53−ordinalize n = T.pack (show n) <> ordinal n
54−{-# INLINE ordinalize #-}
Text/Inflections/Parameterize.hs+4−4
@@ -27,13 +27,13 @@ import qualified Data.Text as T
2727import Control.Applicative
2828#endif
2929
30−-- |Replaces special characters in a string so that it may be used as part of a
31−-- 'pretty' URL. Uses the default transliterations in this library.
30+-- | Replace special characters in a string so that it may be used as part
31+-- of a 'pretty' URL. Uses the 'defaultTransliterations'.
3232parameterize :: Text -> Text
33−parameterize = parameterizeCustom defaultMap
33+parameterize = parameterizeCustom defaultTransliterations
3434{-# INLINE parameterize #-}
3535
36−-- |Transliterate 'Text' with a custom transliteration table.
36+-- | Transliterate 'Text' with a custom transliteration table.
3737parameterizeCustom :: Transliterations -> Text -> Text
3838parameterizeCustom m txt = (T.intercalate "-" . T.words) (T.unfoldr f ("", txt))
3939 where
Text/Inflections/Parse/Acronym.hsdeleted−32
@@ -1,32 +0,0 @@
1−-- |
2−-- Module : Text.Inflections.Parse.Acronym
3−-- Copyright : © 2016 Justin Leitgeb
4−-- License : MIT
5−--
6−-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7−-- Stability : experimental
8−-- Portability : portable
9−--
10−-- Parser for acronyms.
11−
12−{-# LANGUAGE CPP #-}
13−
14−module Text.Inflections.Parse.Acronym
15− ( acronym )
16−where
17−
18−import Data.Text (Text)
19−import Text.Inflections.Parse.Types
20−import Text.Megaparsec
21−import Text.Megaparsec.Text
22−import qualified Data.Text as T
23−
24−#if MIN_VERSION_base(4,8,0)
25−import Prelude hiding (Word)
26−#endif
27−
28−-- | Parser that accepts a 'Text' from given collection and turns it into
29−-- an 'Acronym'.
30−acronym :: [Text] -> Parser Word
31−acronym = fmap (Acronym . T.pack) . choice . fmap (string . T.unpack)
32−{-# INLINE acronym #-}
Text/Inflections/Parse/CamelCase.hs+36−17
@@ -10,15 +10,16 @@
1010-- Parser for camel case “symbols”.
1111
1212{-# LANGUAGE CPP #-}
13+{-# LANGUAGE DataKinds #-}
1314{-# LANGUAGE OverloadedStrings #-}
1415
1516module Text.Inflections.Parse.CamelCase
1617 ( parseCamelCase )
1718where
1819
20+import Control.Applicative
1921import Data.Text (Text)
20−import Text.Inflections.Parse.Acronym (acronym)
21−import Text.Inflections.Parse.Types (Word(..))
22+import Text.Inflections.Types
2223import Text.Megaparsec
2324import Text.Megaparsec.Text
2425import qualified Data.Text as T
@@ -26,31 +27,49 @@ import qualified Data.Text as T
2627#if MIN_VERSION_base(4,8,0)
2728import Prelude hiding (Word)
2829#else
29−import Control.Applicative
30+import Data.Foldable
31+import Prelude hiding (elem)
3032#endif
3133
32−-- |Parse a CamelCase string.
34+-- | Parse a CamelCase string.
3335--
34−-- >>> parseCamelCase ["Bar"] "FooBarBazz"
36+-- >>> bar <- mkAcronym "bar"
37+-- >>> parseCamelCase [bar] "FooBarBazz"
3538-- Right [Word "Foo",Acronym "Bar",Word "Bazz"]
39+--
3640-- >>> parseCamelCase [] "foo_bar_bazz"
37−-- Left "(unknown)" (line 1, column 4):
41+-- 1:4:
3842-- unexpected '_'
39−parseCamelCase
40− :: [Text] -- ^ Collection of acronyms
41− -> Text -- ^ Input
42− -> Either (ParseError Char Dec) [Word] -- ^ Result of parsing
43+-- expecting end of input, lowercase letter, or uppercase letter
44+parseCamelCase :: (Foldable f, Functor f)
45+ => f (Word 'Acronym) -- ^ Collection of acronyms
46+ -> Text -- ^ Input
47+ -> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
4348parseCamelCase acronyms = parse (parser acronyms) ""
4449
45−parser
46− :: [Text] -- ^ Collection of acronyms
47− -> Parser [Word] -- ^ CamelCase parser
48−parser acronyms = many (acronym acronyms <|> word) <* eof
49−{-# INLINE parser #-}
50+parser :: (Foldable f, Functor f)
51+ => f (Word 'Acronym) -- ^ Collection of acronyms
52+ -> Parser [SomeWord] -- ^ CamelCase parser
53+parser acronyms = many (a <|> n) <* eof
54+ where
55+ n = SomeWord <$> word
56+ a = SomeWord <$> acronym acronyms
57+
58+acronym :: (Foldable f, Functor f)
59+ => f (Word 'Acronym)
60+ -> Parser (Word 'Acronym)
61+acronym acronyms = do
62+ x <- T.pack <$> choice (string . T.unpack . unWord <$> acronyms)
63+ case mkAcronym x of
64+ Nothing -> empty -- cannot happen if the system is sound
65+ Just acr -> return acr
66+{-# INLINE acronym #-}
5067
51−word :: Parser Word
68+word :: Parser (Word 'Normal)
5269word = do
5370 firstChar <- upperChar <|> lowerChar
5471 restChars <- many $ lowerChar <|> digitChar
55− return . Word . T.pack $ firstChar : restChars
72+ case (mkWord . T.pack) (firstChar : restChars) of
73+ Nothing -> empty -- cannot happen if the system is sound
74+ Just wrd -> return wrd
5675{-# INLINE word #-}
Text/Inflections/Parse/SnakeCase.hs+28−21
@@ -9,15 +9,16 @@
99--
1010-- Parser for snake case “symbols”.
1111
12−{-# LANGUAGE CPP #-}
12+{-# LANGUAGE CPP #-}
13+{-# LANGUAGE DataKinds #-}
1314
1415module Text.Inflections.Parse.SnakeCase
1516 ( parseSnakeCase )
1617where
1718
19+import Control.Applicative
1820import Data.Text (Text)
19−import Text.Inflections.Parse.Acronym (acronym)
20−import Text.Inflections.Parse.Types (Word(..))
21+import Text.Inflections.Types
2122import Text.Megaparsec
2223import Text.Megaparsec.Text
2324import qualified Data.Text as T
@@ -25,31 +26,37 @@ import qualified Data.Text as T
2526#if MIN_VERSION_base(4,8,0)
2627import Prelude hiding (Word)
2728#else
28−import Control.Applicative
29+import Data.Foldable
30+import Prelude hiding (elem)
2931#endif
3032
31−-- |Parses a snake_case string.
33+-- | Parse a snake_case string.
3234--
33−-- >>> parseSnakeCase ["bar"] "foo_bar_bazz"
35+-- >>> bar <- mkAcronym "bar"
36+-- >>> parseSnakeCase [bar] "foo_bar_bazz"
3437-- Right [Word "foo",Acronym "bar",Word "bazz"]
38+--
3539-- >>> parseSnakeCase [] "fooBarBazz"
36−-- Left "(unknown)" (line 1, column 4):
40+-- 1:4:
3741-- unexpected 'B'
38−parseSnakeCase
39− :: [Text] -- ^ Collection of acronyms
42+-- expecting '_', end of input, or lowercase letter
43+parseSnakeCase :: (Foldable f, Functor f)
44+ => f (Word 'Acronym) -- ^ Collection of acronyms
4045 -> Text -- ^ Input
41− -> Either (ParseError Char Dec) [Word] -- ^ Result of parsing
46+ -> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
4247parseSnakeCase acronyms = parse (parser acronyms) ""
4348
44−parser
45− :: [Text]
46− -> Parser [Word]
47−parser acronyms = do
48− ws <- (acronym acronyms <|> word) `sepBy` char '_'
49− eof
50− return ws
51−{-# INLINE parser #-}
49+parser :: (Foldable f, Functor f)
50+ => f (Word 'Acronym)
51+ -> Parser [SomeWord]
52+parser acronyms = (pWord acronyms `sepBy` char '_') <* eof
5253
53−word :: Parser Word
54−word = Word . T.pack <$> (some lowerChar <|> some digitChar)
55−{-# INLINE word #-}
54+pWord :: (Foldable f, Functor f)
55+ => f (Word 'Acronym)
56+ -> Parser SomeWord
57+pWord acronyms = do
58+ let acs = unWord <$> acronyms
59+ r <- T.pack <$> some alphaNumChar
60+ if r `elem` acs
61+ then maybe empty (return . SomeWord) (mkAcronym r)
62+ else maybe empty (return . SomeWord) (mkWord r)
Text/Inflections/Parse/Types.hsdeleted−54
@@ -1,54 +0,0 @@
1−-- |
2−-- Module : Text.Inflections.Parse.Types
3−-- Copyright : © 2016 Justin Leitgeb
4−-- License : MIT
5−--
6−-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7−-- Stability : experimental
8−-- Portability : portable
9−--
10−-- Types used in the library.
11−
12−{-# LANGUAGE CPP #-}
13−{-# LANGUAGE DeriveDataTypeable #-}
14−
15−module Text.Inflections.Parse.Types
16− ( Word (..) -- FIXME we should not export the constructor
17− , unWord
18− , mapWord
19− , InflectionException (..) )
20−where
21−
22−import Control.Monad.Catch (Exception)
23−import Data.Text (Text)
24−import Data.Typeable (Typeable)
25−import Text.Megaparsec
26−
27−#if MIN_VERSION_base(4,8,0)
28−import Prelude hiding (Word)
29−#endif
30−
31−-- | A 'String' that should be kept whole through applied inflections
32−data Word
33− = Word Text -- ^ A word that may be transformed by inflection
34− | Acronym Text -- ^ A word that may not be transformed by inflections
35− deriving (Show, Eq)
36−
37−-- | Get a 'Text' value from 'Word'.
38−unWord :: Word -> Text
39−unWord (Word s) = s
40−unWord (Acronym s) = s
41−{-# INLINE unWord #-}
42−
43−-- | Apply 'Text' transforming function to a 'Word' unless it's a 'Acronym'.
44−mapWord :: (Text -> Text) -> Word -> Text
45−mapWord f (Word s) = f s
46−mapWord _ (Acronym s) = s
47−{-# INLINE mapWord #-}
48−
49−-- | The exceptions that is thrown when parsing of input fails.
50−
51−data InflectionException = InflectionParsingFailed (ParseError Char Dec)
52− deriving (Eq, Show, Typeable)
53−
54−instance Exception InflectionException
Text/Inflections/Titleize.hs+8−12
@@ -9,26 +9,22 @@
99--
1010-- Conversion to titleized phrases.
1111
12−{-# LANGUAGE CPP #-}
13−
1412module Text.Inflections.Titleize
1513 ( titleize )
1614where
1715
1816import Data.Text (Text)
19−import Text.Inflections.Parse.Types
17+import Text.Inflections.Types
2018import qualified Data.Text as T
2119
22−#if MIN_VERSION_base(4,8,0)
23−import Prelude hiding (Word)
24−#endif
25−
2620-- | Capitalize all the Words in the input list.
2721--
28−-- >>> titleize [ Word "foo", Acronym "bar", Word "bazz" ]
22+-- >>> foo <- SomeWord <$> mkWord "foo"
23+-- >>> bar <- SomeWord <$> mkAcronym "bar"
24+-- >>> bazz <- SomeWord <$> mkWord "bazz"
25+-- >>> titleize [foo,bar,bazz]
2926-- "Foo bar Bazz"
3027titleize
31− :: [Word] -- ^ List of Words, first of which will be capitalized
32− -> Text -- ^ The titleized String
33−titleize = T.unwords . fmap (mapWord T.toTitle)
34−{-# INLINE titleize #-}
28+ :: [SomeWord] -- ^ List of words, first of which will be capitalized
29+ -> Text -- ^ The titleized 'Text'
30+titleize = T.unwords . fmap (unSomeWord T.toTitle)
Text/Inflections/Transliterate.hs+3−4
@@ -27,15 +27,14 @@ import qualified Data.Text as T
2727import Control.Applicative
2828#endif
2929
30−-- |Returns a String after default approximations for changing Unicode
30+-- | Returns a 'Text' after default approximations for changing Unicode
3131-- characters to a valid ASCII range are applied. If you want to supplement
3232-- the default approximations with your own, you should use the
3333-- 'transliterateCustom' function instead of 'transliterate'.
3434transliterate :: Text -> Text
35−transliterate = transliterateCustom "?" defaultMap
36−{-# INLINE transliterate #-}
35+transliterate = transliterateCustom "?" defaultTransliterations
3736
38−-- |Returns a String after default approximations for changing Unicode
37+-- | Returns a 'Text' after default approximations for changing Unicode
3938-- characters to a valid ASCII range are applied.
4039transliterateCustom
4140 :: String -- ^ The default replacement
Text/Inflections/Types.hsnew file+143
@@ -0,0 +1,143 @@
1+-- |
2+-- Module : Text.Inflections.Types
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Types used in the library. Usually you don't need to import this module
11+-- and "Text.Inflections" should be enough.
12+
13+{-# LANGUAGE CPP #-}
14+{-# LANGUAGE DataKinds #-}
15+{-# LANGUAGE DeriveDataTypeable #-}
16+{-# LANGUAGE DeriveGeneric #-}
17+{-# LANGUAGE FlexibleContexts #-}
18+{-# LANGUAGE FlexibleInstances #-}
19+{-# LANGUAGE GADTs #-}
20+{-# LANGUAGE KindSignatures #-}
21+
22+module Text.Inflections.Types
23+ ( Word
24+ , WordType (..)
25+ , mkWord
26+ , mkAcronym
27+ , unWord
28+ , SomeWord (..)
29+ , unSomeWord
30+ , InflectionException (..) )
31+where
32+
33+import Control.Monad.Catch
34+import Data.Char (isAlphaNum)
35+import Data.Data (Data)
36+import Data.Text (Text)
37+import Data.Typeable (Typeable)
38+import GHC.Generics (Generic)
39+import Text.Megaparsec
40+import qualified Data.Text as T
41+
42+#if MIN_VERSION_base(4,8,0)
43+import Prelude hiding (Word)
44+#endif
45+
46+-- | Create a word from given 'Text'. The input should consist of only
47+-- alpha-numeric characters (no white spaces or punctuation)
48+-- 'InflectionInvalidWord' will be thrown.
49+--
50+-- /since 0.3.0.0/
51+
52+mkWord :: MonadThrow m => Text -> m (Word 'Normal)
53+mkWord txt =
54+ if T.all isAlphaNum txt
55+ then return (Word txt)
56+ else throwM (InflectionInvalidWord txt)
57+
58+-- | Create an acronym from given 'Text'. The input should consist of only
59+-- alpha-numeric characters 'InflectionInvalidAcronym' will be thrown.
60+-- Acronym is different from normal word by that it may not be transformed
61+-- by inflections (also see 'unSomeWord').
62+--
63+-- /since 0.3.0.0/
64+
65+mkAcronym :: MonadThrow m => Text -> m (Word 'Acronym)
66+mkAcronym txt =
67+ if T.all isAlphaNum txt
68+ then return (Word txt)
69+ else throwM (InflectionInvalidAcronym txt)
70+
71+-- | A 'Text' value that should be kept whole through applied inflections.
72+
73+data Word (t :: WordType) = Word Text
74+ deriving (Eq, Ord)
75+
76+instance Show (Word 'Normal) where
77+ show (Word x) = "Word " ++ show x
78+
79+instance Show (Word 'Acronym) where
80+ show (Word x) = "Acronym " ++ show x
81+
82+-- | A type-level tag for words.
83+--
84+-- /since 0.3.0.0/
85+
86+data WordType = Normal | Acronym
87+
88+-- | Get a 'Text' value from 'Word'.
89+--
90+-- /since 0.3.0.0/
91+
92+unWord :: Word t -> Text
93+unWord (Word s) = s
94+
95+-- | An existential wrapper that allows to keep words and acronyms in single
96+-- list for example. The only thing that receiver of 'SomeWord' can do is to
97+-- apply 'unWord' on it, of course. This is faciliated by 'unSomeWord'.
98+--
99+-- /since 0.3.0.0/
100+
101+data SomeWord where
102+ SomeWord :: (Transformable (Word t), Show (Word t)) => Word t -> SomeWord
103+ -- NOTE The constraint is only needed because GHC is not smart enough
104+ -- (yet?) to figure out that t cannot be anything other than Normal and
105+ -- Acronym and thus all cases are already covered by the instances
106+ -- provided below.
107+
108+instance Show SomeWord where
109+ show (SomeWord w) = show w
110+
111+instance Eq SomeWord where
112+ x == y = unSomeWord id x == unSomeWord id y
113+
114+-- | Extract 'Text' from 'SomeWord' and apply given function only if the
115+-- word inside wasn't an acronym.
116+--
117+-- /since 0.3.0.0/
118+
119+unSomeWord :: (Text -> Text) -> SomeWord -> Text
120+unSomeWord f (SomeWord w) = transform f w
121+
122+-- | Non public stuff.
123+
124+class Transformable a where
125+ transform :: (Text -> Text) -> a -> Text
126+
127+instance Transformable (Word 'Normal) where
128+ transform f = f . unWord
129+
130+instance Transformable (Word 'Acronym) where
131+ transform _ = unWord
132+
133+-- | The exceptions that is thrown when parsing of input fails.
134+--
135+-- /since 0.3.0.0/
136+
137+data InflectionException
138+ = InflectionParsingFailed (ParseError Char Dec)
139+ | InflectionInvalidWord Text
140+ | InflectionInvalidAcronym Text
141+ deriving (Eq, Show, Typeable, Data, Generic)
142+
143+instance Exception InflectionException
Text/Inflections/Underscore.hs+9−12
@@ -9,7 +9,6 @@
99--
1010-- Conversion to phrases separated by underscores.
1111
12−{-# LANGUAGE CPP #-}
1312{-# LANGUAGE OverloadedStrings #-}
1413
1514module Text.Inflections.Underscore
@@ -17,19 +16,17 @@ module Text.Inflections.Underscore
1716where
1817
1918import Data.Text (Text)
20−import Text.Inflections.Parse.Types
19+import Text.Inflections.Types
2120import qualified Data.Text as T
2221
23−#if MIN_VERSION_base(4,8,0)
24−import Prelude hiding (Word)
25−#endif
26−
27−-- |Turns a CamelCase string into an underscore_separated 'Text'.
22+-- | Separate given words by underscores.
2823--
29−-- >>> underscore [ Word "foo", Acronym "bar", Word "bazz" ]
24+-- >>> foo <- SomeWord <$> mkWord "foo"
25+-- >>> bar <- SomeWord <$> mkAcronym "bar"
26+-- >>> bazz <- SomeWord <$> mkWord "bazz"
27+-- >>> underscore [foo,bar,bazz]
3028-- "foo_bar_bazz"
3129underscore
32− :: [Word] -- ^ Input Words to separate with underscores
33− -> Text -- ^ The underscored String
34−underscore = T.intercalate "_" . fmap (mapWord T.toLower)
35−{-# INLINE underscore #-}
30+ :: [SomeWord] -- ^ Input words to separate with underscores
31+ -> Text -- ^ The underscored String
32+underscore = T.intercalate "_" . fmap (unSomeWord T.toLower)
inflections.cabal+6−3
@@ -29,7 +29,6 @@ flag dev
2929
3030library
3131 exposed-modules: Text.Inflections
32− , Text.Inflections.Parse.Types
3332
3433 other-modules: Text.Inflections.Data
3534 , Text.Inflections.Camelize
@@ -37,11 +36,11 @@ library
3736 , Text.Inflections.Humanize
3837 , Text.Inflections.Ordinal
3938 , Text.Inflections.Parameterize
40− , Text.Inflections.Parse.Acronym
4139 , Text.Inflections.Parse.CamelCase
4240 , Text.Inflections.Parse.SnakeCase
4341 , Text.Inflections.Titleize
4442 , Text.Inflections.Transliterate
43+ , Text.Inflections.Types
4544 , Text.Inflections.Underscore
4645
4746 if flag(dev)
@@ -63,7 +62,8 @@ test-suite test
6362 , QuickCheck >= 2.7.6 && < 3.0
6463 , base >= 4.6 && < 5.0
6564 , hspec >= 2.0 && < 3.0
66− , megaparsec >= 5.0 && < 6.0
65+ , hspec-megaparsec >= 0.3 && < 0.4
66+ , megaparsec >= 5.1 && < 6.0
6767 , text >= 0.2 && < 1.3
6868 if flag(dev)
6969 ghc-options: -Wall -Werror
@@ -74,7 +74,10 @@ test-suite test
7474 , Text.Inflections.HumanizeSpec
7575 , Text.Inflections.OrdinalSpec
7676 , Text.Inflections.ParametrizeSpec
77+ , Text.Inflections.Parse.CamelCaseSpec
78+ , Text.Inflections.Parse.SnakeCaseSpec
7779 , Text.Inflections.TitleizeSpec
7880 , Text.Inflections.TransliterateSpec
81+ , Text.Inflections.TypesSpec
7982 , Text.Inflections.UnderscoreSpec
8083 , Text.InflectionsSpec
stack.yaml+3
@@ -1,3 +1,6 @@
11resolver: lts-7.13
22packages:
33- '.'
4+extra-deps:
5+- hspec-megaparsec-0.3.0
6+- megaparsec-5.1.2
test/Text/Inflections/DasherizeSpec.hs+10−4
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE OverloadedStrings #-}
23
34module Text.Inflections.DasherizeSpec
@@ -6,10 +7,15 @@ where
67
78import Test.Hspec
89
9−import Text.Inflections (dasherize)
10−import Text.Inflections.Parse.Types (Word (..))
10+import Text.Inflections
11+
12+#if !MIN_VERSION_base(4,8,0)
13+import Control.Applicative
14+#endif
1115
1216spec :: Spec
1317spec = describe "dasherize" $
14− it "dasherizes a collection of words sentence" $
15− dasherize [Word "foo", Word "bar"] `shouldBe` "foo-bar"
18+ it "dasherizes a collection of words sentence" $ do
19+ foo <- SomeWord <$> mkWord "foo"
20+ bar <- SomeWord <$> mkWord "bar"
21+ dasherize [foo,bar] `shouldBe` "foo-bar"
test/Text/Inflections/HumanizeSpec.hs+18−8
@@ -1,17 +1,27 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE OverloadedStrings #-}
23
34module Text.Inflections.HumanizeSpec (spec) where
45
56import Test.Hspec
67
7−import Text.Inflections (humanize)
8−import Text.Inflections.Parse.Types (Word(..))
8+import Text.Inflections
9+
10+#if !MIN_VERSION_base(4,8,0)
11+import Control.Applicative
12+#endif
913
1014spec :: Spec
1115spec = describe "humazine" $ do
12− it "converts snake case to a human-readable string" $
13− humanize [Word "employee", Word "salary"] `shouldBe` "Employee salary"
14− it "turns underscores into spaces" $
15− humanize [Word "employee", Word "has_salary"] `shouldBe` "Employee has salary"
16− it "capitalizes the first word of a sentence" $
17− humanize [Word "underground"] `shouldBe` "Underground"
16+ it "converts snake case to a human-readable string" $ do
17+ employee <- SomeWord <$> mkWord "employee"
18+ salary <- SomeWord <$> mkWord "salary"
19+ humanize [employee,salary] `shouldBe` "Employee salary"
20+ it "turns underscores into spaces" $ do
21+ employee <- SomeWord <$> mkWord "employee"
22+ has <- SomeWord <$> mkWord "has"
23+ salary <- SomeWord <$> mkWord "salary"
24+ humanize [employee, has, salary] `shouldBe` "Employee has salary"
25+ it "capitalizes the first word of a sentence" $ do
26+ underground <- SomeWord <$> mkWord "underground"
27+ humanize [underground] `shouldBe` "Underground"
test/Text/Inflections/Parse/CamelCaseSpec.hsnew file+47
@@ -0,0 +1,47 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Text.Inflections.Parse.CamelCaseSpec
4+ ( spec )
5+where
6+
7+import Test.Hspec
8+import Test.Hspec.Megaparsec
9+import Text.Inflections
10+
11+spec :: Spec
12+spec =
13+ describe "parseCamelCase" $ do
14+ context "when given no acronyms" $ do
15+ context "when first word is capitalized" $
16+ it "parses CamelCase correctly" $ do
17+ r <- mapM (fmap SomeWord . mkWord) ["One","Two","Three"]
18+ parseCamelCase [] "OneTwoThree" `shouldParse` r
19+ context "when first word is not capitalized" $
20+ it "parses camelCase correctly" $ do
21+ r <- mapM (fmap SomeWord . mkWord) ["one","Two","Three"]
22+ parseCamelCase [] "oneTwoThree" `shouldParse` r
23+ context "when there are digits in the words" $
24+ it "parses CamelCase correctly" $ do
25+ r <- mapM (fmap SomeWord . mkWord) ["one1two","Three3"]
26+ parseCamelCase [] "one1twoThree3" `shouldParse` r
27+ context "when given some acronyms" $ do
28+ context "when first word is capitalized" $
29+ it "parses CamelCase correctly" $ do
30+ a <- mkAcronym "BOO"
31+ r <- mapM (fmap SomeWord . mkWord) ["One","BOO","One"]
32+ parseCamelCase [a] "OneBOOOne" `shouldParse` r
33+ context "when first word is not capitalized" $
34+ it "parses camelCase correctly" $ do
35+ a <- mkAcronym "BOO"
36+ r <- mapM (fmap SomeWord . mkWord) ["one","Two","BOO"]
37+ parseCamelCase [a] "oneTwoBOO" `shouldParse` r
38+ context "when there are digits in the words" $
39+ it "parses CamelCase correctly" $ do
40+ a <- mkAcronym "BOO"
41+ r <- mapM (fmap SomeWord . mkWord) ["one1two","Three3"]
42+ parseCamelCase [a] "one1twoThree3" `shouldParse` r
43+ context "when a word has a suffix coinciding with a acronym" $
44+ it "is still parsed correctly as a normala word" $ do
45+ a <- mkAcronym "boo"
46+ r <- mapM (fmap SomeWord . mkWord) ["fooboo","Bar"]
47+ parseCamelCase [a] "foobooBar" `shouldParse` r
test/Text/Inflections/Parse/SnakeCaseSpec.hsnew file+35
@@ -0,0 +1,35 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Text.Inflections.Parse.SnakeCaseSpec
4+ ( spec )
5+where
6+
7+import Test.Hspec
8+import Test.Hspec.Megaparsec
9+import Text.Inflections
10+
11+spec :: Spec
12+spec =
13+ describe "parseSnakeCase" $ do
14+ context "when given no acronyms" $ do
15+ it "parses snake_case correctly" $ do
16+ r <- mapM (fmap SomeWord . mkWord) ["OneTwo","Three"]
17+ parseSnakeCase [] "OneTwo_Three" `shouldParse` r
18+ it "handles digits in the words correctly" $ do
19+ r <- mapM (fmap SomeWord . mkWord) ["one4a","two00"]
20+ parseSnakeCase [] "one4a_two00" `shouldParse` r
21+ context "when given some acronyms" $ do
22+ it "parses snake_case correctly" $ do
23+ a <- mkAcronym "BOO"
24+ r <- mapM (fmap SomeWord . mkWord) ["BOO","one","one"]
25+ parseSnakeCase [a] "BOO_one_one" `shouldParse` r
26+ context "when acronym happens to be a prefix of other word" $
27+ it "parses the word correctly" $ do
28+ a <- mkAcronym "BOO"
29+ r <- mapM (fmap SomeWord . mkWord) ["one","BOOtwo"]
30+ parseSnakeCase [a] "one_BOOtwo" `shouldParse` r
31+ context "when acronym happens to be a suffix of other word" $
32+ it "parses the word correctly" $ do
33+ a <- mkAcronym "BOO"
34+ r <- mapM (fmap SomeWord . mkWord) ["oneBOO","two"]
35+ parseSnakeCase [a] "oneBOO_two" `shouldParse` r
test/Text/Inflections/TitleizeSpec.hs+12−6
@@ -1,15 +1,21 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE OverloadedStrings #-}
23
34module Text.Inflections.TitleizeSpec (spec) where
45
56import Test.Hspec
7+import Text.Inflections
68
7−import Text.Inflections (titleize)
8−import Text.Inflections.Parse.Types (Word(..))
9+#if !MIN_VERSION_base(4,8,0)
10+import Control.Applicative
11+#endif
912
1013spec :: Spec
1114spec = describe "titleize" $ do
12− it "converts two words to title case" $
13− titleize [Word "Employee", Word "Salary"] `shouldBe` "Employee Salary"
14− it "converts one word to title case" $
15− titleize [Word "underground"] `shouldBe` "Underground"
15+ it "converts two words to title case" $ do
16+ employee <- SomeWord <$> mkWord "Employee"
17+ salary <- SomeWord <$> mkWord "Salary"
18+ titleize [employee,salary] `shouldBe` "Employee Salary"
19+ it "converts one word to title case" $ do
20+ underground <- SomeWord <$> mkWord "underground"
21+ titleize [underground] `shouldBe` "Underground"
test/Text/Inflections/TypesSpec.hsnew file+51
@@ -0,0 +1,51 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+module Text.Inflections.TypesSpec
5+ ( spec )
6+where
7+
8+import Test.Hspec
9+import Text.Inflections
10+import qualified Data.Text as T
11+
12+#if !MIN_VERSION_base(4,8,0)
13+import Control.Applicative
14+#endif
15+
16+spec :: Spec
17+spec = do
18+ describe "mkWord" $ do
19+ context "when provided a correct Text value" $
20+ it "creates a normal word" $ do
21+ x <- mkWord "foo"
22+ unWord x `shouldBe` "foo"
23+ context "when provided an incorrect Text value" $
24+ it "throws the correct exception" $ do
25+ mkWord "foo bar" `shouldThrow` (== InflectionInvalidWord "foo bar")
26+ mkWord "foo_" `shouldThrow` (== InflectionInvalidWord "foo_")
27+ mkWord "&$?%" `shouldThrow` (== InflectionInvalidWord "&$?%")
28+ describe "mkAcronym" $ do
29+ context "when provided a correct Text value" $
30+ it "creates an acronym" $ do
31+ x <- mkAcronym "foo"
32+ unWord x `shouldBe` "foo"
33+ context "when providde an incorrect Text value" $
34+ it "throws the correct exception" $ do
35+ mkAcronym "foo bar" `shouldThrow` (== InflectionInvalidAcronym "foo bar")
36+ mkAcronym "foo_" `shouldThrow` (== InflectionInvalidAcronym "foo_")
37+ mkAcronym "&$?%" `shouldThrow` (== InflectionInvalidAcronym "&$?%")
38+ describe "unWord" $
39+ it "extracts the inner Text value" $ do
40+ (unWord <$> mkWord "foo") `shouldReturn` "foo"
41+ (unWord <$> mkWord "bar") `shouldReturn` "bar"
42+ (unWord <$> mkAcronym "baz") `shouldReturn` "baz"
43+ describe "unSomeWord" $ do
44+ context "when inner value is a normal word" $
45+ it "Text is extracted and the given function applied" $ do
46+ x <- SomeWord <$> mkWord "word"
47+ unSomeWord T.toUpper x `shouldBe` "WORD"
48+ context "when inner value is an acronym" $
49+ it "Text is extracted, but the function is not applied" $ do
50+ x <- SomeWord <$> mkAcronym "acronym"
51+ unSomeWord T.toUpper x `shouldBe` "acronym"
test/Text/Inflections/UnderscoreSpec.hs+10−4
@@ -1,12 +1,18 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE OverloadedStrings #-}
23
34module Text.Inflections.UnderscoreSpec (spec) where
45
56import Test.Hspec
6−import Text.Inflections (underscore)
7−import Text.Inflections.Parse.Types (Word(..))
7+import Text.Inflections
8+
9+#if !MIN_VERSION_base(4,8,0)
10+import Control.Applicative
11+#endif
812
913spec :: Spec
1014spec = describe "underscore" $
11− it "converts a word list to snake case" $
12− underscore [Word "test", Word "this"] `shouldBe` "test_this"
15+ it "converts a word list to snake case" $ do
16+ test <- SomeWord <$> mkWord "test"
17+ this <- SomeWord <$> mkWord "this"
18+ underscore [test, this] `shouldBe` "test_this"
test/Text/InflectionsSpec.hs+22−7
@@ -3,22 +3,37 @@
33module Text.InflectionsSpec (spec) where
44
55import Test.Hspec
6−import Text.Inflections (toUnderscore, toDashed, toCamelCased)
6+import Test.QuickCheck
7+import Text.Inflections
78
89spec :: Spec
910spec = do
11+
1012 describe "toUnderscore" $ do
1113 it "converts camel case to snake case" $
12− toUnderscore "camelCasedText" `shouldReturn` "camel_cased_text"
14+ toUnderscore "camelCasedText" `shouldBe` Right "camel_cased_text"
1315 it "converts camel case to snake case with numbers" $
14− toUnderscore "ipv4Address" `shouldReturn` "ipv4_address"
16+ toUnderscore "ipv4Address" `shouldBe` Right "ipv4_address"
17+
1518 describe "toDashed" $
1619 it "converts camel case to dashed" $
17− toDashed "camelCasedText" `shouldReturn` "camel-cased-text"
20+ toDashed "camelCasedText" `shouldBe` Right "camel-cased-text"
21+
1822 describe "toCamelCased" $ do
1923 context "when the first argument is False" $
2024 it "converts snake case to camel case" $
21− toCamelCased False "underscored_text" `shouldReturn` "underscoredText"
25+ toCamelCased False "underscored_text" `shouldBe` Right "underscoredText"
2226 context "when the first argument is True" $
23− it "converts snake case to camel case" $
24− toCamelCased True "underscored_text" `shouldReturn` "UnderscoredText"
27+ it "converts snake case to camel case with the first word capitalized" $
28+ toCamelCased True "underscored_text" `shouldBe` Right "UnderscoredText"
29+
30+ describe "betterThrow" $ do
31+ context "when given a parse error" $
32+ it "throws the correct exception" $
33+ property $ \err ->
34+ betterThrow (Left err) `shouldThrow`
35+ (== InflectionParsingFailed err)
36+ context "when given a value in Right" $
37+ it "returns the value" $
38+ property $ \x ->
39+ betterThrow (Right x) `shouldReturn` (x :: Int)