Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Adding functions to TH to make CamelCase to under_score easier. #155

Merged
merged 5 commits into from

4 participants

@gabrielmc

I have noticed in multiple places around hackage that people add
these kinds of functions to get their JSON formatted keys (often
underscore_formatted_strings) out of Haskell's CamelCase data
properties (often withLongStringsOfCAPITALS).

I have also added to the .gitignore those files generated by the new
cabal 1.18 'sandbox' feature.

I have a few outstanding questions:

1) Should these functions live in another module with an isomorphic
inverse function, like underscoreToCamel? Like Aeson.Properties?
2) I am at something of a loss as to how to integrate testing into the
suite. I've tested the out at the REPL, but what steps should be
taken to integrate these into QuickCheck?

Gabe McArthur Adding functions to TH to make CamelCase to under_score easier.
I have noticed in multiple places around hackage that people add
these kinds of functions to get their JSON formatted keys (often
underscore_formatted_strings) out of Haskell's CamelCase data
properties (often withLongStringsOfCAPITALS).

I have also added to the .gitignore those files generated by the new
cabal 1.18 'sandbox' feature.

I have a few outstanding questions:

1) Should these functions live in another module with an isomorphic
inverse function, like `underscoreToCamel`? Like Aeson.Properties?
2) I am at something of a loss as to how to integrate testing into the
suite. I've tested the out at the REPL, but what steps should be
taken to integrate these into QuickCheck?
74e8b1b
@basvandijk
Collaborator

Hi @gabemc , this looks like a useful function to have. A few comments:

  • I would define this function near the same place as were the Options type is defined. That's currently the Data.Aeson.Types.Internal module. (Note that not only the TH deriver uses the Options type but also the GHC Generics based deriver)

  • It would be nice if the character to replace the camelCasing could be parameterized as in:

    camelTo :: Char -> (String -> String)
  • Why combine the dropping of the prefix with underscoring. I think it's easier if the user just uses function composition as in:

    defaultOptions {fieldLabelModifier = camelTo '_' . drop 4}

What do you think?

@gabrielmc

Good idea, @basvandijk. I like it much better. However, my question about testing remains. I've tested it, and it seems to work, but that hardly seems sufficient. Where would be the best place to insert a test about its functionality?

@basvandijk
Collaborator

I like it much better.

I like it to! Note you have a small spelling error: s/interpsersing/interspersing/.

However, my question about testing remains. I've tested it, and it seems to work, but that hardly seems sufficient. Where would be the best place to insert a test about its functionality?

You should add a test to the test-suite. Its main file is tests/Properties.hs

Gabe McArthur added some commits
@bos
Owner

If you can rebase these patches so that they apply cleanly, I'll snag 'em.

@gabrielmc

Ok, I think that should do it.

@bos bos merged commit be4cfc8 into from
@hvr

@basvandijk

Why combine the dropping of the prefix with underscoring. I think it's easier if the user just uses function composition as in:

defaultOptions {fieldLabelModifier = camelTo '_' . drop 4}

fyi, while that's easier, I use something like the following in my code, as I want to catch typos (which can easily creep in when I copy'n'paste and forget to update the drop 4 to the proper prefix-length):

deCaml :: String -> String -> String
deCaml pfx s0
  | Just (c:cs) <- stripPrefix pfx s0, isUpper c = toLower c : toSnakeCase cs
  | otherwise = error "deCaml prefix-mismatch"
  where
    toSnakeCase = concatMap (\c -> if isUpper c then ['_',toLower c] else [c])
@basvandijk
Collaborator

I agree that drop 4 is not the best way to drop prefixes. In fact, I use the following at work for this:

delPrefix :: String -> (String -> String)
delPrefix prefix = \fieldName ->
    case stripPrefix prefix fieldName of
      Just ccs@(c:cs)
          | isUpper c -> toLower c : cs
          | null prefix -> ccs
          | otherwise -> error $ "The field name after the prefix " ++
                                 "must be written in CamelCase"
      Just [] -> error $ "The field name after the prefix may not be empty"
      Nothing -> error $ "The field name " ++ quotes fieldName ++
                         " does not begin with the required prefix " ++
                         quotes prefix

What I was arguing for was to not combine the dropping of a prefix with decamelcasification. They can simply be composed together if desired:

camelTo '_' . delPrefix "prefix"
@hvr

What I was arguing for was to not combine the dropping of a prefix with decamelcasification. They can simply be composed together

+1 to that :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 13, 2013
  1. Adding functions to TH to make CamelCase to under_score easier.

    Gabe McArthur authored
    I have noticed in multiple places around hackage that people add
    these kinds of functions to get their JSON formatted keys (often
    underscore_formatted_strings) out of Haskell's CamelCase data
    properties (often withLongStringsOfCAPITALS).
    
    I have also added to the .gitignore those files generated by the new
    cabal 1.18 'sandbox' feature.
    
    I have a few outstanding questions:
    
    1) Should these functions live in another module with an isomorphic
    inverse function, like `underscoreToCamel`? Like Aeson.Properties?
    2) I am at something of a loss as to how to integrate testing into the
    suite. I've tested the out at the REPL, but what steps should be
    taken to integrate these into QuickCheck?
Commits on Oct 26, 2013
Commits on Oct 27, 2013
  1. Fixing a typo.

    Gabe McArthur authored
  2. Adding tests for the camlCase operation.

    Gabe McArthur authored
Commits on Nov 29, 2013
  1. @gabrielmc

    Merge branch 'master' of https://github.com/bos/aeson

    gabrielmc authored
    Conflicts:
    	Data/Aeson/Types/Internal.hs
This page is out of date. Refresh to see the latest.
View
4 .gitignore
@@ -1,4 +1,6 @@
dist
+.cabal-sandbox/
+cabal.sandbox.config
*.o
*.hi
@@ -7,4 +9,4 @@ benchmarks/AesonEncode
tests/qc
-cabal.sandbox.config
+cabal.sandbox.config
View
27 Data/Aeson/Types/Internal.hs
@@ -36,13 +36,18 @@ module Data.Aeson.Types.Internal
, defaultOptions
, defaultTaggedObject
+ -- * Used for changing CamelCase names into something else.
+ , camelTo
+
-- * Other types
, DotNetTime(..)
) where
+
import Control.Applicative
import Control.Monad
import Control.DeepSeq (NFData(..))
+import Data.Char (toLower, isUpper)
import Data.Scientific (Scientific)
import Data.Hashable (Hashable(..))
import Data.HashMap.Strict (HashMap)
@@ -342,3 +347,25 @@ defaultTaggedObject = TaggedObject
{ tagFieldName = "tag"
, contentsFieldName = "contents"
}
+
+-- | Converts from CamelCase to another lower case, interspersing
+-- the character between all capital letters and their previous
+-- entries, except those capital letters that appear together,
+-- like 'API'.
+--
+-- For use by Aeson template haskell calls.
+--
+-- > camelTo '_' 'CamelCaseAPI' == "camel_case_api"
+camelTo :: Char -> String -> String
+camelTo c = lastWasCap True
+ where
+ lastWasCap :: Bool -- ^ Previous was a capital letter
+ -> String -- ^ The remaining string
+ -> String
+ lastWasCap _ [] = []
+ lastWasCap prev (x : xs) = if isUpper x
+ then if prev
+ then toLower x : lastWasCap True xs
+ else c : toLower x : lastWasCap True xs
+ else x : lastWasCap False xs
+
View
9 tests/Properties.hs
@@ -22,6 +22,11 @@ import Data.Int
import qualified Data.Map as Map
#endif
+roundTripCaml :: String -> Bool
+roundTripCaml s = s == (camlFrom '_' $ camlTo '_' s)
+ where
+ camlFrom :: Char -> String -> String
+ camlFrom c = concatMap capitalize $ split c
encodeDouble :: Double -> Double -> Bool
encodeDouble num denom
@@ -101,6 +106,10 @@ tests = [
testProperty "encodeDouble" encodeDouble
, testProperty "encodeInteger" encodeInteger
],
+ testGroup "camlCase" [
+ testProperty "camlTo" $ roundTripCaml "AnApiMethod"
+ , testProperty "camlTo" $ roundTripCaml "anotherMethodType"
+ ],
testGroup "roundTrip" [
testProperty "Bool" $ roundTripEq True
, testProperty "Double" $ roundTripEq (1 :: Approx Double)
Something went wrong with that request. Please try again.