@@ -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