@@ -1,171 +1,202 @@
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 Text 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 'Text' 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 'Text' that makes 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 'Text' 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− , defaultMap
86−
87− , parameterize
88− , parameterizeCustom
89−
90− , transliterate
91− , transliterateCustom
92−
93− , ordinal
94− , ordinalize
95−
96− , parseSnakeCase
97− , parseCamelCase
98− , Transliterations
82+ ( -- * Types and helpers
83+ Word
84+ , WordType (..)
85+ , mkWord
86+ , mkAcronym
87+ , unWord
88+ , SomeWord (..)
89+ , unSomeWord
90+ , InflectionException (..)
91+ -- * Parsing
92+ , parseSnakeCase
93+ , parseCamelCase
94+ -- * Rendering
95+ , camelize
96+ , camelizeCustom
97+ , dasherize
98+ , humanize
99+ , humanizeCustom
100+ , underscore
101+ , titleize
102+ , Transliterations
103+ , defaultTransliterations
104+ , parameterize
105+ , parameterizeCustom
106+ , transliterate
107+ , transliterateCustom
108+ , ordinalize
109+ , ordinal
99110 -- * Often used combinators
100− , toUnderscore
101− , toDashed
102− , toCamelCased
103− )
111+ , toUnderscore
112+ , toDashed
113+ , toCamelCased
114+ , toHumanized
115+ , betterThrow )
104116where
105117
106−
107−import Text.Inflections.Data ( defaultMap )
108−
109−import Text.Inflections.Parameterize ( Transliterations
110− , parameterize
111− , parameterizeCustom )
112−
113−import Text.Inflections.Underscore ( underscore )
114−
115−import Text.Inflections.Camelize ( camelize, camelizeCustom )
116−
117−import Text.Inflections.Humanize ( humanize )
118−
119−import Text.Inflections.Titleize ( titleize )
120−
121−import Text.Inflections.Transliterate ( transliterate, transliterateCustom )
122−
123−import Text.Inflections.Dasherize ( dasherize )
124−
125−import Text.Inflections.Ordinal ( ordinal, ordinalize )
126−
127−import Text.Inflections.Parse.SnakeCase ( parseSnakeCase )
128−
129−import Text.Inflections.Parse.Types ( mapWord )
130−
131−import Text.Inflections.Parse.CamelCase ( parseCamelCase )
132−
133−import Data.Char ( toLower )
134−
135−-- | Transforms CamelCasedString to
136−-- snake_cased_string_with_underscores. Throws exception if parsing failed
118+import Control.Monad.Catch (MonadThrow (..))
119+import Data.Text (Text)
120+import Data.Void (Void)
121+import Text.Inflections.Camelize (camelize, camelizeCustom)
122+import Text.Inflections.Dasherize (dasherize)
123+import Text.Inflections.Data (Transliterations, defaultTransliterations)
124+import Text.Inflections.Humanize (humanize, humanizeCustom)
125+import Text.Inflections.Ordinal (ordinal, ordinalize)
126+import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
127+import Text.Inflections.Parse.CamelCase (parseCamelCase)
128+import Text.Inflections.Parse.SnakeCase (parseSnakeCase)
129+import Text.Inflections.Titleize (titleize)
130+import Text.Inflections.Transliterate (transliterate, transliterateCustom)
131+import Text.Inflections.Types
132+import Text.Inflections.Underscore (underscore)
133+import Text.Megaparsec
134+
135+#if MIN_VERSION_base(4,8,0)
136+import Prelude hiding (Word)
137+#endif
138+
139+-- | Transforms CamelCasedString to snake_cased_string_with_underscores.
140+--
141+-- > toUnderscore = fmap underscore . parseCamelCase []
137142--
138143-- >>> toUnderscore "FooBarBazz"
139144-- "foo_bar_bazz"
140−toUnderscore :: String -> String
141−toUnderscore =
142− underscore
143− . either (error . ("toUnderscore: " ++) . show) id
144− . parseCamelCase []
145+toUnderscore :: Text -> Either (ParseErrorBundle Text Void) Text
146+toUnderscore = fmap underscore . parseCamelCase []
145147
146−-- | Transforms CamelCasedString to snake-cased-string-with-dashes. Throws
147−-- exception if parsing failed.
148+-- | Transforms CamelCasedString to snake-cased-string-with-dashes.
149+--
150+-- > toDashed = fmap dasherize . parseCamelCase []
148151--
149152-- >>> toDashed "FooBarBazz"
150153-- "foo-bar-bazz"
151−toDashed :: String -> String
152−toDashed =
153− dasherize
154− . map (mapWord (map toLower))
155− . either (error . ("toDashed: " ++) . show) id
156− . parseCamelCase []
154+toDashed :: Text -> Either (ParseErrorBundle Text Void) Text
155+toDashed = fmap dasherize . parseCamelCase []
157156
158157-- | Transforms underscored_text to CamelCasedText. If first argument is
159−-- 'True' then FirstCharacter in result string will be in upper case. If
160−-- 'False' then firstCharacter will be in lower case. Throws exception if
161−-- parsing failed
158+-- 'True' then the first character in the result string will be in upper case. If
159+-- 'False' then the first character will be in lower case.
160+--
161+-- > toCamelCased c = fmap (camelizeCustom c) . parseSnakeCase []
162162--
163163-- >>> toCamelCased True "foo_bar_bazz"
164164-- "FooBarBazz"
165165-- >>> toCamelCased False "foo_bar_bazz"
166166-- "fooBarBazz"
167−toCamelCased :: Bool -> String -> String
168−toCamelCased t =
169− camelizeCustom t
170− . either (error . ("toCamelCased: " ++) . show) id
171− . parseSnakeCase []
167+toCamelCased
168+ :: Bool -- ^ Capitalize the first character
169+ -> Text -- ^ Input
170+ -> Either (ParseErrorBundle Text Void) Text -- ^ Output
171+toCamelCased c = fmap (camelizeCustom c) . parseSnakeCase []
172+
173+-- | Transforms underscored_text to space-separated human-readable text.
174+-- If first argument is 'True' then the first character in the result
175+-- string will be in upper case. If 'False' then the first character will be
176+-- in lower case.
177+--
178+-- > toHumanized c = fmap (humanizeCustom c) . parseSnakeCase []
179+--
180+-- >>> toHumanized True "foo_bar_bazz"
181+-- "Foo bar bazz"
182+-- >>> toHumanized False "foo_bar_bazz"
183+-- "foo bar bazz"
184+--
185+-- /since 0.3.0.0/
186+toHumanized
187+ :: Bool -- ^ Capitalize the first character
188+ -> Text -- ^ Input
189+ -> Either (ParseErrorBundle Text Void) Text -- ^ Output
190+toHumanized c = fmap (humanizeCustom c) . parseSnakeCase []
191+
192+-- | Lift something of type @'Either' ('ParseError' 'Char' 'Void') a@ to
193+-- an instance of 'MonadThrow'. Useful when you want to shortcut on parsing
194+-- failures and you're in an instance of 'MonadThrow'.
195+--
196+-- This throws 'InflectionParsingFailed' if given value is inside 'Left'.
197+--
198+-- /since 0.3.0.0/
199+
200+betterThrow :: MonadThrow m => Either (ParseErrorBundle Text Void) a -> m a
201+betterThrow (Left err) = throwM (InflectionParsingFailed err)
202+betterThrow (Right x) = return x