on master help sign in
Merge branch 'master' into dockerci
851dc76AgentM
docs/tupleable.markdown+19
@@ -23,6 +23,25 @@ instance Tupleable Person
23
23
24Now, ```Person``` values can be marshaled between the database and the client. The attribute names appearing in the Haskell data type will appear identically in the server with the same types.
24Now, ```Person``` values can be marshaled between the database and the client. The attribute names appearing in the Haskell data type will appear identically in the server with the same types.
25
25
26+### Changing Options
27+
28+If our field names don't exactly match attribute names, but follow a common pattern, we can derive Tupleable with custom options. Let's say each field on person has a prefix "person", but we still want to use the attribute names without the prefix. We can make use of the ProjectM36.Tupleable module to accomplish our goal like so:
29+
30+```haskell
31+import ProjectM36.Tupleable.Deriving
32+
33+data Person = Person {
34+ personName :: Text,
35+ personEmployeeNumber :: Int,
36+ personLikesHorses :: Bool
37+ }
38+ deriving stock Generic
39+ deriving Tupleable
40+ via Codec (Field (DropPrefix "person" >>> CamelCase)) Person
41+```
42+
43+The extensions DerivingGeneric, DerivingVia, TypeOperators, and DataKinds must be enabled for this code to compile. See the documentation on Hackage for all available options.
44+
26## Usage
45## Usage
27
46
28The ```Tupleable``` module includes the following functions:
47The ```Tupleable``` module includes the following functions:
examples/DerivingCustomTupleable.hsnew file+52
@@ -0,0 +1,52 @@
1+{-# LANGUAGE DeriveGeneric, DerivingVia, GeneralizedNewtypeDeriving, TypeOperators, DataKinds, OverloadedStrings #-}
2+
3+import ProjectM36.Tupleable.Deriving
4+import ProjectM36.Atomable (Atomable)
5+import Control.DeepSeq (NFData)
6+import Data.Binary (Binary)
7+import Data.Text (Text)
8+import GHC.Generics (Generic)
9+
10+newtype BlogId = BlogId { getBlogId :: Int }
11+ deriving stock (Eq, Ord, Show, Generic)
12+ deriving newtype (Num)
13+
14+instance NFData BlogId
15+instance Binary BlogId
16+instance Atomable BlogId
17+
18+data Blog = Blog
19+ { blogId :: BlogId
20+ , blogTitle :: Text
21+ , blogAuthorName :: Text
22+ }
23+ deriving stock (Show, Generic)
24+ deriving (Tupleable)
25+ via Codec (Field (DropPrefix "blog" >>> CamelCase)) Blog
26+
27+data Comment = Comment
28+ { commentAuthorName :: Text
29+ , commentComment :: Text
30+ , commentFor :: BlogId
31+ }
32+ deriving stock (Show, Generic)
33+ deriving (Tupleable)
34+ via Codec (Field (DropPrefix "comment" >>> CamelCase)) Comment
35+
36+main :: IO ()
37+main = do
38+ let exampleBlog = Blog
39+ { blogId = 0
40+ , blogTitle = "Cat Pics"
41+ , blogAuthorName = "Alice"
42+ }
43+ exampleComment = Comment
44+ { commentAuthorName = "Bob"
45+ , commentComment = "great"
46+ , commentFor = 0
47+ }
48+ print exampleBlog
49+ print (toTuple exampleBlog)
50+ putStrLn ""
51+ print exampleComment
52+ print (toTuple exampleComment)
project-m36.cabal+12−1
@@ -34,7 +34,7 @@ Flag haskell-scripting
34 Default: True
34 Default: True
35
35
36Library
36Library
37− Build-Depends: base>=4.8 && < 4.14, ghc-paths, mtl, containers, unordered-containers, hashable, haskeline, directory, MonadRandom, random-shuffle, uuid >= 1.3.12, cassava >= 0.4.5.1 && < 0.6, text, bytestring, deepseq, deepseq-generics, vector, parallel, monad-parallel, exceptions, transformers, gnuplot, binary, filepath, zlib, directory, vector-binary-instances, temporary, stm, time, hashable-time, old-locale, rset, attoparsec, either, base64-bytestring, data-interval, extended-reals, network-transport, aeson >= 1.1, path-pieces, conduit, resourcet, http-api-data, semigroups, QuickCheck, quickcheck-instances, distributed-process-client-server >= 0.2.3, distributed-process >= 0.7.4, distributed-process-extras >= 0.3.2, distributed-process-async >= 0.2.4.1, network-transport-tcp >= 0.6.0, network-transport, list-t, stm-containers >= 0.2.15, foldl, optparse-applicative, Glob, cryptohash-sha256
37+ Build-Depends: base>=4.8 && < 4.14, ghc-paths, mtl, containers, unordered-containers, hashable, haskeline, directory, MonadRandom, random-shuffle, uuid >= 1.3.12, cassava >= 0.4.5.1 && < 0.6, text, bytestring, deepseq, deepseq-generics, vector, parallel, monad-parallel, exceptions, transformers, gnuplot, binary, filepath, zlib, directory, vector-binary-instances, temporary, stm, time, hashable-time, old-locale, rset, attoparsec, either, base64-bytestring, data-interval, extended-reals, network-transport, aeson >= 1.1, path-pieces, conduit, resourcet, http-api-data, semigroups, QuickCheck, quickcheck-instances, distributed-process-client-server >= 0.2.3, distributed-process >= 0.7.4, distributed-process-extras >= 0.3.2, distributed-process-async >= 0.2.4.1, network-transport-tcp >= 0.6.0, network-transport, list-t, stm-containers >= 0.2.15, foldl, optparse-applicative, Glob, cryptohash-sha256, text-manipulate >= 0.2.0.1 && < 0.3
38 if flag(haskell-scripting)
38 if flag(haskell-scripting)
39 Build-Depends: ghc >= 8.2 && < 8.9
39 Build-Depends: ghc >= 8.2 && < 8.9
40 CPP-Options: -DPM36_HASKELL_SCRIPTING
40 CPP-Options: -DPM36_HASKELL_SCRIPTING
@@ -89,6 +89,7 @@ Library
89 ProjectM36.AtomFunctions.Primitive,
89 ProjectM36.AtomFunctions.Primitive,
90 ProjectM36.Atomable,
90 ProjectM36.Atomable,
91 ProjectM36.Tupleable,
91 ProjectM36.Tupleable,
92+ ProjectM36.Tupleable.Deriving,
92 ProjectM36.DataConstructorDef,
93 ProjectM36.DataConstructorDef,
93 ProjectM36.DataTypes.Basic,
94 ProjectM36.DataTypes.Basic,
94 ProjectM36.DataTypes.Sorting,
95 ProjectM36.DataTypes.Sorting,
@@ -410,6 +411,16 @@ Executable Example-CustomTupleable
410 Main-Is: examples/CustomTupleable.hs
411 Main-Is: examples/CustomTupleable.hs
411 GHC-Options: -Wall -threaded
412 GHC-Options: -Wall -threaded
412
413
414+Executable Example-DerivingCustomTupleable
415+ if impl(ghc>=8.6)
416+ Buildable: True
417+ else
418+ Buildable: False
419+ Default-Language: Haskell2010
420+ Build-Depends: base, text, deepseq, binary, project-m36
421+ Main-Is: examples/DerivingCustomTupleable.hs
422+ GHC-Options: -Wall -threaded
423+
413Test-Suite test-scripts
424Test-Suite test-scripts
414 Default-Language: Haskell2010
425 Default-Language: Haskell2010
415 Default-Extensions: OverloadedStrings
426 Default-Extensions: OverloadedStrings
src/lib/ProjectM36/Tupleable.hs+102−26
@@ -6,7 +6,26 @@
6{-# LANGUAGE UndecidableInstances #-}
6{-# LANGUAGE UndecidableInstances #-}
7{-# LANGUAGE CPP #-}
7{-# LANGUAGE CPP #-}
8
8
9−module ProjectM36.Tupleable where
9+module ProjectM36.Tupleable
10+ ( toInsertExpr
11+ , toDefineExpr
12+ , tupleAssocsEqualityPredicate
13+ , partitionByAttributes
14+ , toUpdateExpr
15+ , toDeleteExpr
16+ , validateAttributes
17+ , Tupleable(..)
18+
19+ -- * Generics
20+ , genericToTuple
21+ , genericFromTuple
22+ , genericToAttributes
23+ , TupleableG(..)
24+ -- ** Options
25+ , defaultTupleableOptions
26+ , TupleableOptions()
27+ , fieldModifier
28+ ) where
10
29
11import Data.Foldable
30import Data.Foldable
12import Data.List (partition)
31import Data.List (partition)
@@ -121,56 +140,109 @@ validateAttributes actualAttrs expectedAttrs val
121 where
140 where
122 nonMatchingAttrs = attributeNamesNotContained actualAttrs expectedAttrs
141 nonMatchingAttrs = attributeNamesNotContained actualAttrs expectedAttrs
123
142
124−
143+-- | Types that can be converted to and from 'RelationTuple'.
144+--
145+-- deriving without customization:
146+--
147+-- > data Example = Example
148+-- > { foo :: Integer
149+-- > , bar :: Text
150+-- > }
151+-- > deriving (Generic)
152+-- >
153+-- > instance Tupleable Example
154+--
155+-- deriving with customization using "ProjectM36.Tupleable.Deriving":
156+--
157+-- > data Example = Example
158+-- > { exampleFoo :: Integer
159+-- > , exampleBar :: Text
160+-- > }
161+-- > deriving stock (Generic)
162+-- > deriving (Tupleable)
163+-- > via Codec (Field (DropPrefix "example" >>> CamelCase)) Example
125class Tupleable a where
164class Tupleable a where
126 toTuple :: a -> RelationTuple
165 toTuple :: a -> RelationTuple
127
166
128 fromTuple :: RelationTuple -> Either RelationalError a
167 fromTuple :: RelationTuple -> Either RelationalError a
129
168
130− toAttributes :: proxy a -> Attributes
169+ toAttributes :: Proxy a -> Attributes
131
170
132 default toTuple :: (Generic a, TupleableG (Rep a)) => a -> RelationTuple
171 default toTuple :: (Generic a, TupleableG (Rep a)) => a -> RelationTuple
133− toTuple v = toTupleG (from v)
172+ toTuple = genericToTuple defaultTupleableOptions
134
173
135 default fromTuple :: (Generic a, TupleableG (Rep a)) => RelationTuple -> Either RelationalError a
174 default fromTuple :: (Generic a, TupleableG (Rep a)) => RelationTuple -> Either RelationalError a
175+ fromTuple = genericFromTuple defaultTupleableOptions
176+
177+ default toAttributes :: (Generic a, TupleableG (Rep a)) => Proxy a -> Attributes
178+ toAttributes = genericToAttributes defaultTupleableOptions
179+
180+-- | Options that influence deriving behavior.
181+newtype TupleableOptions = TupleableOptions {
182+ -- | A function that translates record field names into attribute names.
183+ fieldModifier :: T.Text -> T.Text
184+ }
185+
186+-- | The default options for deriving Tupleable instances.
187+--
188+-- These options can be customized by using record update syntax. For example,
189+--
190+-- > defaultTupleableOptions
191+-- > { fieldModifier = \fieldName ->
192+-- > case Data.Text.stripPrefix "example" fieldName of
193+-- > Nothing -> fieldName
194+-- > Just attributeName -> attributeName
195+-- > }
196+--
197+-- will result in record field names being translated into attribute names by
198+-- removing the prefix "example" from the field names.
199+defaultTupleableOptions :: TupleableOptions
200+defaultTupleableOptions = TupleableOptions {
201+ fieldModifier = id
202+ }
203+
204+genericToTuple :: (Generic a, TupleableG (Rep a)) => TupleableOptions -> a -> RelationTuple
205+genericToTuple opts v = toTupleG opts (from v)
206+
207+genericFromTuple :: (Generic a, TupleableG (Rep a)) => TupleableOptions -> RelationTuple -> Either RelationalError a
136− fromTuple tup = to <$> fromTupleG tup
208+genericFromTuple opts tup = to <$> fromTupleG opts tup
137
209
138− default toAttributes :: (Generic a, TupleableG (Rep a)) => proxy a -> Attributes
210+genericToAttributes :: forall a. (Generic a, TupleableG (Rep a)) => TupleableOptions -> Proxy a -> Attributes
139− toAttributes _ = toAttributesG (from (undefined :: a))
211+genericToAttributes opts _ = toAttributesG opts (from (undefined :: a))
140
212
141class TupleableG g where
213class TupleableG g where
142− toTupleG :: g a -> RelationTuple
214+ toTupleG :: TupleableOptions -> g a -> RelationTuple
143− toAttributesG :: g a -> Attributes
215+ toAttributesG :: TupleableOptions -> g a -> Attributes
144− fromTupleG :: RelationTuple -> Either RelationalError (g a)
216+ fromTupleG :: TupleableOptions -> RelationTuple -> Either RelationalError (g a)
145 isRecordTypeG :: g a -> Bool
217 isRecordTypeG :: g a -> Bool
146
218
147--data type metadata
219--data type metadata
148instance (Datatype c, TupleableG a) => TupleableG (M1 D c a) where
220instance (Datatype c, TupleableG a) => TupleableG (M1 D c a) where
149− toTupleG (M1 v) = toTupleG v
221+ toTupleG opts (M1 v) = toTupleG opts v
150− toAttributesG (M1 v) = toAttributesG v
222+ toAttributesG opts (M1 v) = toAttributesG opts v
151− fromTupleG v = M1 <$> fromTupleG v
223+ fromTupleG opts v = M1 <$> fromTupleG opts v
152 isRecordTypeG (M1 v) = isRecordTypeG v
224 isRecordTypeG (M1 v) = isRecordTypeG v
153
225
154--constructor metadata
226--constructor metadata
155instance (Constructor c, TupleableG a, AtomableG a) => TupleableG (M1 C c a) where
227instance (Constructor c, TupleableG a, AtomableG a) => TupleableG (M1 C c a) where
156− toTupleG (M1 v) = RelationTuple attrs atoms
228+ toTupleG opts (M1 v) = RelationTuple attrs atoms
157 where
229 where
158− attrsToCheck = toAttributesG v
230+ attrsToCheck = toAttributesG opts v
159 counter = V.generate (V.length attrsToCheck) id
231 counter = V.generate (V.length attrsToCheck) id
160 attrs = V.zipWith (\num attr@(Attribute name typ) -> if T.null name then
232 attrs = V.zipWith (\num attr@(Attribute name typ) -> if T.null name then
161 Attribute ("attr" <> T.pack (show (num + 1))) typ
233 Attribute ("attr" <> T.pack (show (num + 1))) typ
162 else
234 else
163 attr) counter attrsToCheck
235 attr) counter attrsToCheck
164 atoms = V.fromList (toAtomsG v)
236 atoms = V.fromList (toAtomsG v)
165− toAttributesG (M1 v) = toAttributesG v
237+ toAttributesG opts (M1 v) = toAttributesG opts v
166− fromTupleG tup = M1 <$> fromTupleG tup
238+ fromTupleG opts tup = M1 <$> fromTupleG opts tup
167 isRecordTypeG (M1 v) = isRecordTypeG v
239 isRecordTypeG (M1 v) = isRecordTypeG v
168
240
169-- product types
241-- product types
170instance (TupleableG a, TupleableG b) => TupleableG (a :*: b) where
242instance (TupleableG a, TupleableG b) => TupleableG (a :*: b) where
171 toTupleG = error "toTupleG"
243 toTupleG = error "toTupleG"
172− toAttributesG ~(x :*: y) = toAttributesG x V.++ toAttributesG y --a bit of extra laziness prevents whnf so that we can use toAttributes (undefined :: Test2T Int Int) without throwing an exception
244+ toAttributesG opts ~(x :*: y) = toAttributesG opts x V.++ toAttributesG opts y --a bit of extra laziness prevents whnf so that we can use toAttributes (undefined :: Test2T Int Int) without throwing an exception
173− fromTupleG tup = (:*:) <$> fromTupleG tup <*> fromTupleG processedTuple
245+ fromTupleG opts tup = (:*:) <$> fromTupleG opts tup <*> fromTupleG opts processedTuple
174 where
246 where
175 processedTuple = if isRecordTypeG (undefined :: a x) then
247 processedTuple = if isRecordTypeG (undefined :: a x) then
176 tup
248 tup
@@ -181,18 +253,22 @@ instance (TupleableG a, TupleableG b) => TupleableG (a :*: b) where
181--selector/record
253--selector/record
182instance (Selector c, AtomableG a) => TupleableG (M1 S c a) where
254instance (Selector c, AtomableG a) => TupleableG (M1 S c a) where
183 toTupleG = error "toTupleG"
255 toTupleG = error "toTupleG"
184− toAttributesG m@(M1 v) = V.singleton (Attribute name aType)
256+ toAttributesG opts m@(M1 v) = V.singleton (Attribute modifiedName aType)
185 where
257 where
186 name = T.pack (selName m)
258 name = T.pack (selName m)
259+ modifiedName = if T.null name then
260+ name
261+ else
262+ fieldModifier opts name
187 aType = toAtomTypeG v
263 aType = toAtomTypeG v
188− fromTupleG tup = if null name then -- non-record type, just pull off the first tuple item
264+ fromTupleG opts tup = if null name then -- non-record type, just pull off the first tuple item
189 M1 <$> atomv (V.head (tupleAtoms tup))
265 M1 <$> atomv (V.head (tupleAtoms tup))
190 else do
266 else do
191− atom <- atomForAttributeName (T.pack name) tup
267+ atom <- atomForAttributeName (fieldModifier opts (T.pack name)) tup
192 val <- atomv atom
268 val <- atomv atom
193 pure (M1 val)
269 pure (M1 val)
194 where
270 where
195− expectedAtomType = atomType (V.head (toAttributesG (undefined :: M1 S c a x)))
271+ expectedAtomType = atomType (V.head (toAttributesG opts (undefined :: M1 S c a x)))
196 atomv atom = maybe (Left (AtomTypeMismatchError
272 atomv atom = maybe (Left (AtomTypeMismatchError
197 expectedAtomType
273 expectedAtomType
198 (atomTypeForAtom atom)
274 (atomTypeForAtom atom)
@@ -203,8 +279,8 @@ instance (Selector c, AtomableG a) => TupleableG (M1 S c a) where
203--constructors with no arguments
279--constructors with no arguments
204--basically useless but orthoganal to relationTrue
280--basically useless but orthoganal to relationTrue
205instance TupleableG U1 where
281instance TupleableG U1 where
206− toTupleG _= emptyTuple
282+ toTupleG _ _ = emptyTuple
207− toAttributesG _ = emptyAttributes
283+ toAttributesG _ _ = emptyAttributes
208− fromTupleG _ = pure U1
284+ fromTupleG _ _ = pure U1
209 isRecordTypeG _ = False
285 isRecordTypeG _ = False
210
286
src/lib/ProjectM36/Tupleable/Deriving.hsnew file+229
@@ -0,0 +1,229 @@
1+{-# LANGUAGE ScopedTypeVariables #-}
2+{-# LANGUAGE TypeOperators #-}
3+{-# LANGUAGE KindSignatures #-}
4+{-# LANGUAGE DataKinds #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE UndecidableInstances #-}
7+
8+-- | Newtypes for deriving Tupleable instances with customization using
9+-- @DerivingVia@.
10+--
11+-- Inspired by
12+-- [Dhall.Deriving](https://hackage.haskell.org/package/dhall-1.33.1/docs/Dhall-Deriving.html)
13+-- which in turn was inspired by Matt Parson's blog post
14+-- [Mirror Mirror: Reflection and Encoding Via](https://www.parsonsmatt.org/2020/02/04/mirror_mirror.html).
15+--
16+-- required extensions:
17+--
18+-- * DerivingVia
19+-- * DeriveGenerics
20+-- * TypeOperators (for @('<<<')@ and @('>>>')@)
21+-- * DataKinds (for types that take a string argument)
22+
23+module ProjectM36.Tupleable.Deriving
24+ ( -- * DerivingVia Newtype
25+ Codec(..)
26+
27+ -- * Type-level Options
28+ , ModifyOptions(..)
29+ , Field
30+
31+ -- * Type-level 'T.Text' -> 'T.Text' Functions
32+ , ModifyText(..)
33+ , AddPrefix
34+ , DropPrefix
35+ , AddSuffix
36+ , DropSuffix
37+ , UpperCase
38+ , LowerCase
39+ , TitleCase
40+ , CamelCase
41+ , PascalCase
42+ , SnakeCase
43+ , SpinalCase
44+ , TrainCase
45+
46+ -- * Composition
47+ , AsIs
48+ , type (<<<)
49+ , type (>>>)
50+
51+ -- * Re-Exports
52+ , Generic
53+ , module ProjectM36.Tupleable
54+ ) where
55+import Data.Maybe (fromMaybe)
56+import Data.Proxy
57+import qualified Data.Text as T
58+import Data.Text.Manipulate
59+import GHC.TypeLits
60+import GHC.Generics (Generic, Rep)
61+import ProjectM36.Tupleable
62+
63+
64+-- | A newtype wrapper to allow for easier deriving of 'Tupleable' instances
65+-- with customization.
66+--
67+-- The @tag@ type variable can be used to specify options for converting the
68+-- datatype to and from a 'RelationTuple'. For example,
69+--
70+-- > data Example = Example
71+-- > { exampleFoo :: Int
72+-- > , exampleBar :: Int
73+-- > }
74+-- > deriving stock (Generic)
75+-- > deriving (Tupleable)
76+-- > via Codec (Field (DropPrefix "example" >>> CamelCase)) Example
77+--
78+-- will derive an instance of 'Tupleable' where field names are translated into
79+-- attribute names by dropping the prefix @"example"@ and then converting the
80+-- result to camelCase. So @"exampleFoo"@ becomes @"foo"@ and @"exampleBar"@
81+-- becomes @"bar"@.
82+--
83+-- Requires the @DerivingGeneric@ and @DerivingVia@ extensions to be enabled.
84+newtype Codec tag a = Codec { unCodec :: a }
85+
86+instance (ModifyOptions tag, Generic a, TupleableG (Rep a)) => Tupleable (Codec tag a) where
87+ toTuple v = genericToTuple opts (unCodec v)
88+ where
89+ opts = modifyOptions (Proxy :: Proxy tag) defaultTupleableOptions
90+
91+ fromTuple tup = Codec <$> genericFromTuple opts tup
92+ where
93+ opts = modifyOptions (Proxy :: Proxy tag) defaultTupleableOptions
94+
95+ toAttributes _ = genericToAttributes opts (Proxy :: Proxy a)
96+ where
97+ opts = modifyOptions (Proxy :: Proxy tag) defaultTupleableOptions
98+
99+-- | Types that can be used as tags for 'Codec'.
100+class ModifyOptions a where
101+ modifyOptions :: proxy a -> TupleableOptions -> TupleableOptions
102+
103+-- | Change how record field names are translated into attribute names. For
104+-- example,
105+--
106+-- > Field SnakeCase
107+--
108+-- will translate the field name @fooBar@ into the attribute name @foo_bar@.
109+data Field a
110+
111+instance ModifyText a => ModifyOptions (Field a) where
112+ modifyOptions _ opts = opts { fieldModifier = newFieldModifier }
113+ where
114+ newFieldModifier = modifyText (Proxy :: Proxy a) . fieldModifier opts
115+
116+-- | Types that can be used in options that modify 'T.Text' such as in 'Field'.
117+class ModifyText a where
118+ modifyText :: proxy a -> T.Text -> T.Text
119+
120+-- | Add a prefix. @AddPrefix "foo"@ will transform @"bar"@ into @"foobar"@.
121+data AddPrefix (prefix :: Symbol)
122+
123+instance KnownSymbol prefix => ModifyText (AddPrefix prefix) where
124+ modifyText _ oldText = prefixText <> oldText
125+ where
126+ prefixText = T.pack (symbolVal (Proxy :: Proxy prefix))
127+
128+-- | Drop a prefix. @DropPrefix "bar"@ will transform @"foobar"@ into @"foo"@.
129+data DropPrefix (prefix :: Symbol)
130+
131+instance KnownSymbol prefix => ModifyText (DropPrefix prefix) where
132+ modifyText _ oldText = fromMaybe oldText (T.stripPrefix prefixText oldText)
133+ where
134+ prefixText = T.pack (symbolVal (Proxy :: Proxy prefix))
135+
136+-- | Add a suffix. @AddSuffix "bar"@ will transform @"foo"@ into @"foobar"@.
137+data AddSuffix (suffix :: Symbol)
138+
139+instance KnownSymbol suffix => ModifyText (AddSuffix suffix) where
140+ modifyText _ oldText = oldText <> suffixText
141+ where
142+ suffixText = T.pack (symbolVal (Proxy :: Proxy suffix))
143+
144+-- | Drop a suffix. @DropSuffix "bar"@ will transform @"foobar"@ into @"foo"@.
145+data DropSuffix (suffix :: Symbol)
146+
147+instance KnownSymbol suffix => ModifyText (DropSuffix suffix) where
148+ modifyText _ oldText = fromMaybe oldText (T.stripSuffix suffixText oldText)
149+ where
150+ suffixText = T.pack (symbolVal (Proxy :: Proxy suffix))
151+
152+-- | Convert to UPPERCASE. Will transform @"foobar"@ into @\"FOOBAR\"@.
153+data UpperCase
154+
155+instance ModifyText UpperCase where
156+ modifyText _ = T.toUpper
157+
158+-- | Convert to lowercase. Will transform @\"FOOBAR\"@ into @"foobar"@.
159+data LowerCase
160+
161+instance ModifyText LowerCase where
162+ modifyText _ = T.toLower
163+
164+-- | Convert to Title Case. Will transform @"fooBar"@ into @\"Foo Bar\"@.
165+data TitleCase
166+
167+instance ModifyText TitleCase where
168+ modifyText _ = toTitle
169+
170+-- | Convert to camelCase. Will transform @"foo_bar"@ into @"fooBar"@.
171+data CamelCase
172+
173+instance ModifyText CamelCase where
174+ modifyText _ = toCamel
175+
176+-- | Convert to PascalCase. Will transform @"foo_bar"@ into @\"FooBar\"@.
177+data PascalCase
178+
179+instance ModifyText PascalCase where
180+ modifyText _ = toPascal
181+
182+-- | Convert to snake_case. Will transform @"fooBar"@ into @"foo_bar"@.
183+data SnakeCase
184+
185+instance ModifyText SnakeCase where
186+ modifyText _ = toSnake
187+
188+-- | Convert to spinal-case. will transform @"fooBar"@ into @"foo-bar"@.
189+data SpinalCase
190+
191+instance ModifyText SpinalCase where
192+ modifyText _ = toSpinal
193+
194+-- | Convert to Train-Case. Will transform @"fooBar"@ into @\"Foo-Bar\"@.
195+data TrainCase
196+
197+instance ModifyText TrainCase where
198+ modifyText _ = toTrain
199+
200+-- | Identity option.
201+type AsIs = ()
202+
203+instance ModifyOptions () where
204+ modifyOptions _ = id
205+
206+instance ModifyText () where
207+ modifyText _ = id
208+
209+-- | Right to left composition.
210+--
211+-- Requires the @TypeOperators@ extension to be enabled.
212+data a <<< b
213+
214+instance (ModifyOptions a, ModifyOptions b) => ModifyOptions (a <<< b) where
215+ modifyOptions _ = modifyOptions (Proxy :: Proxy a) . modifyOptions (Proxy :: Proxy b)
216+
217+instance (ModifyText a, ModifyText b) => ModifyText (a <<< b) where
218+ modifyText _ = modifyText (Proxy :: Proxy a) . modifyText (Proxy :: Proxy b)
219+
220+-- | Left to right composition.
221+--
222+-- Requires the @TypeOperators@ extension to be enabled.
223+data a >>> b
224+
225+instance (ModifyOptions a, ModifyOptions b) => ModifyOptions (a >>> b) where
226+ modifyOptions _ = modifyOptions (Proxy :: Proxy b) . modifyOptions (Proxy :: Proxy a)
227+
228+instance (ModifyText a, ModifyText b) => ModifyText (a >>> b) where
229+ modifyText _ = modifyText (Proxy :: Proxy b) . modifyText (Proxy :: Proxy a)
stack.ghc8.8.yaml+3−1
@@ -2,7 +2,7 @@
2# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
2# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
3
3
4# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
4# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
5−resolver: lts-15.11
5+resolver: lts-16.4
6
6
7# Local packages, usually specified by relative directory name
7# Local packages, usually specified by relative directory name
8packages:
8packages:
@@ -24,6 +24,8 @@ extra-deps:
24- network-transport-tcp-0.7.0
24- network-transport-tcp-0.7.0
25- megaparsec-7.0.5
25- megaparsec-7.0.5
26- network-2.8.0.1
26- network-2.8.0.1
27+- primitive-extras-0.8
28+- primitive-unlifted-0.1.3.0
27
29
28# Override default flag values for local packages and extra-deps
30# Override default flag values for local packages and extra-deps
29flags:
31flags:
test/Relation/Tupleable.hs+170−3
@@ -1,6 +1,16 @@
1+{-# LANGUAGE
2+ DeriveGeneric
1−{-# LANGUAGE DeriveGeneric, FlexibleInstances, FlexibleContexts, DeriveAnyClass #-}
3+ , FlexibleInstances
4+ , FlexibleContexts
5+ , DeriveAnyClass
6+ , ScopedTypeVariables
7+ , TypeApplications
8+ , DataKinds
9+ , TypeOperators
10+ , AllowAmbiguousTypes
11+ #-}
2import Test.HUnit
12import Test.HUnit
3−import ProjectM36.Tupleable
13+import ProjectM36.Tupleable.Deriving
4import ProjectM36.Atomable
14import ProjectM36.Atomable
5import ProjectM36.Attribute
15import ProjectM36.Attribute
6import ProjectM36.Error
16import ProjectM36.Error
@@ -58,6 +68,13 @@ data Test9T = Test9C
58 attr9C :: Double
68 attr9C :: Double
59 }
69 }
60 deriving (Generic, Show, Eq)
70 deriving (Generic, Show, Eq)
71+
72+data Test10T = Test10C {
73+ longAttrNameA10 :: Integer,
74+ long_attr_name_b10 :: Integer,
75+ longMixed_attr_NameC10 :: Integer
76+ }
77+ deriving (Generic, Show, Eq)
61
78
62instance Tupleable Test1T
79instance Tupleable Test1T
63
80
@@ -83,7 +100,7 @@ main = do
83 if errors tcounts + failures tcounts > 0 then exitFailure else exitSuccess
100 if errors tcounts + failures tcounts > 0 then exitFailure else exitSuccess
84
101
85testList :: Test
102testList :: Test
86−testList = TestList [testADT1, testADT2, testADT3, testADT4, testADT6, testADT7, testADT8, testInsertExpr, testDefineExpr, testUpdateExpr, testUpdateExprEmptyAttrs, testDeleteExpr, testUpdateExprWrongAttr, testReorderedTuple]
103+testList = TestList [testADT1, testADT2, testADT3, testADT4, testADT6, testADT7, testADT8, testInsertExpr, testDefineExpr, testUpdateExpr, testUpdateExprEmptyAttrs, testDeleteExpr, testUpdateExprWrongAttr, testReorderedTuple, testAddPrefixField, testDropPrefixField, testAddSuffixField, testDropSuffixField, testUpperCaseField, testLowerCaseField, testTitleCaseField, testCamelCaseField, testPascalCaseField, testSnakeCaseField, testSpinalCaseField, testTrainCaseField, testAsIsCodec, testAsIsField, testComposeRLCodec, testComposeRLField, testComposeLRCodec, testComposeLRField]
87
104
88testADT1 :: Test
105testADT1 :: Test
89testADT1 = TestCase $ assertEqual "one record constructor" (Right example) (fromTuple (toTuple example))
106testADT1 = TestCase $ assertEqual "one record constructor" (Right example) (fromTuple (toTuple example))
@@ -200,3 +217,153 @@ testReorderedTuple = TestCase $ do
200 attrC = 4}
217 attrC = 4}
201 actual = fromTuple . tupleRev . toTuple $ expected
218 actual = fromTuple . tupleRev . toTuple $ expected
202 assertEqual "reordered tuple" (Right expected) actual
219 assertEqual "reordered tuple" (Right expected) actual
220+
221+testCodec :: forall tag. ModifyOptions tag => String -> [AttributeName] -> Test
222+testCodec msg expected = TestCase $
223+ assertEqual msg (S.fromList expected) actual
224+ where
225+ actual = attributeNameSet $ toAttributes (Proxy :: Proxy (Codec tag Test10T))
226+
227+testAddPrefixField :: Test
228+testAddPrefixField = testCodec
229+ @(Field (AddPrefix "prefix_"))
230+ "codec field add prefix attributes"
231+ ["prefix_longAttrNameA10",
232+ "prefix_long_attr_name_b10",
233+ "prefix_longMixed_attr_NameC10"]
234+
235+testDropPrefixField :: Test
236+testDropPrefixField = testCodec
237+ @(Field (DropPrefix "long"))
238+ "codec field drop prefix"
239+ ["AttrNameA10",
240+ "_attr_name_b10",
241+ "Mixed_attr_NameC10"]
242+
243+testAddSuffixField :: Test
244+testAddSuffixField = testCodec
245+ @(Field (AddSuffix "_suffix"))
246+ "codec field add suffix"
247+ ["longAttrNameA10_suffix",
248+ "long_attr_name_b10_suffix",
249+ "longMixed_attr_NameC10_suffix"]
250+
251+testDropSuffixField :: Test
252+testDropSuffixField = testCodec
253+ @(Field (DropSuffix "10"))
254+ "codec field drop suffix"
255+ ["longAttrNameA",
256+ "long_attr_name_b",
257+ "longMixed_attr_NameC"]
258+
259+testUpperCaseField :: Test
260+testUpperCaseField = testCodec
261+ @(Field UpperCase)
262+ "codec field uppercase"
263+ ["LONGATTRNAMEA10",
264+ "LONG_ATTR_NAME_B10",
265+ "LONGMIXED_ATTR_NAMEC10"]
266+
267+testLowerCaseField :: Test
268+testLowerCaseField = testCodec
269+ @(Field LowerCase)
270+ "codec field lowercase"
271+ ["longattrnamea10",
272+ "long_attr_name_b10",
273+ "longmixed_attr_namec10"]
274+
275+testTitleCaseField :: Test
276+testTitleCaseField = testCodec
277+ @(Field TitleCase)
278+ "codec field title case"
279+ ["Long Attr Name A10",
280+ "Long Attr Name B10",
281+ "Long Mixed Attr Name C10"]
282+
283+testCamelCaseField :: Test
284+testCamelCaseField = testCodec
285+ @(Field CamelCase)
286+ "codec field camel case"
287+ ["longAttrNameA10",
288+ "longAttrNameB10",
289+ "longMixedAttrNameC10"]
290+
291+testPascalCaseField :: Test
292+testPascalCaseField = testCodec
293+ @(Field PascalCase)
294+ "codec field pascal case"
295+ ["LongAttrNameA10",
296+ "LongAttrNameB10",
297+ "LongMixedAttrNameC10"]
298+
299+testSnakeCaseField :: Test
300+testSnakeCaseField = testCodec
301+ @(Field SnakeCase)
302+ "codec field snake case"
303+ ["long_attr_name_a10",
304+ "long_attr_name_b10",
305+ "long_mixed_attr_name_c10"]
306+
307+testSpinalCaseField :: Test
308+testSpinalCaseField = testCodec
309+ @(Field SpinalCase)
310+ "codec field spinal case"
311+ ["long-attr-name-a10",
312+ "long-attr-name-b10",
313+ "long-mixed-attr-name-c10"]
314+
315+testTrainCaseField :: Test
316+testTrainCaseField = testCodec
317+ @(Field TrainCase)
318+ "codec field train case"
319+ ["Long-Attr-Name-A10",
320+ "Long-Attr-Name-B10",
321+ "Long-Mixed-Attr-Name-C10"]
322+
323+testAsIsCodec :: Test
324+testAsIsCodec = testCodec
325+ @AsIs
326+ "codec as is"
327+ ["longAttrNameA10",
328+ "long_attr_name_b10",
329+ "longMixed_attr_NameC10"]
330+
331+testAsIsField :: Test
332+testAsIsField = testCodec
333+ @(Field AsIs)
334+ "codec field as is"
335+ ["longAttrNameA10",
336+ "long_attr_name_b10",
337+ "longMixed_attr_NameC10"]
338+
339+testComposeRLCodec :: Test
340+testComposeRLCodec = testCodec
341+ @(Field UpperCase <<< Field (DropPrefix "long"))
342+ "codec (<<<)"
343+ ["ATTRNAMEA10",
344+ "_ATTR_NAME_B10",
345+ "MIXED_ATTR_NAMEC10"]
346+
347+testComposeRLField :: Test
348+testComposeRLField = testCodec
349+ @(Field (UpperCase <<< DropPrefix "long"))
350+ "codec field (<<<)"
351+ ["ATTRNAMEA10",
352+ "_ATTR_NAME_B10",
353+ "MIXED_ATTR_NAMEC10"]
354+
355+testComposeLRCodec :: Test
356+testComposeLRCodec = testCodec
357+ @(Field (DropPrefix "long") >>> Field UpperCase)
358+ "codec (>>>)"
359+ ["ATTRNAMEA10",
360+ "_ATTR_NAME_B10",
361+ "MIXED_ATTR_NAMEC10"]
362+
363+testComposeLRField :: Test
364+testComposeLRField = testCodec
365+ @(Field (DropPrefix "long" >>> UpperCase))
366+ "codec field (>>>)"
367+ ["ATTRNAMEA10",
368+ "_ATTR_NAME_B10",
369+ "MIXED_ATTR_NAMEC10"]