packages feed

prairie 0.0.2.1 → 0.0.3.0

raw patch · 6 files changed

+336/−41 lines, 6 filesdep +hspecPVP ok

version bump matches the API change (PVP)

Dependencies added: hspec

API changes (from Hackage documentation)

+ Prairie.Fold: [SomeFieldWithValue] :: Field rec a -> a -> SomeFieldWithValue rec
+ Prairie.Fold: data SomeFieldWithValue rec
+ Prairie.Fold: foldMRecord :: forall rec m r. (Record rec, Monad m) => (forall ty. ty -> r -> Field rec ty -> m r) -> r -> rec -> m r
+ Prairie.Fold: foldMapRecord :: forall rec m. (Record rec, Monoid m) => (forall ty. ty -> Field rec ty -> m) -> rec -> m
+ Prairie.Fold: foldRecord :: forall rec r. Record rec => (forall ty. ty -> r -> Field rec ty -> r) -> r -> rec -> r
+ Prairie.Fold: recordToFieldList :: forall rec. Record rec => rec -> [SomeFieldWithValue rec]
+ Prairie.Traverse: traverseRecord :: forall rec f. (Record rec, Applicative f) => (forall ty. ty -> Field rec ty -> f ty) -> rec -> f rec

Files

ChangeLog.md view
@@ -1,8 +1,14 @@ # Changelog for prairie +## 0.0.3.0++- [#8](https://github.com/parsonsmatt/prairie/pull/8)+    - The `Prairie.Fold` module is introduced, allowing you to fold records.+    - The `Prairie.Traverse` module is introduced, allowing you to traverse over records.+ ## 0.0.2.1 -- [#3](https://github.com/parsonsmatt/prairie/pull/6)+- [#6](https://github.com/parsonsmatt/prairie/pull/6)     - Bump upper bound for `TemplateHaskell`, supporting up to GHC 9.8     - Fix string literals in docs that were quoted as module names 
prairie.cabal view
@@ -1,7 +1,7 @@ cabal-version: 1.12  name:           prairie-version:        0.0.2.1+version:        0.0.3.0 description:    Please see the README on GitHub at <https://github.com/parsonsmatt/prairie#readme> homepage:       https://github.com/parsonsmatt/prairie#readme bug-reports:    https://github.com/parsonsmatt/prairie/issues@@ -27,6 +27,8 @@       Prairie.Class       Prairie.Update       Prairie.Diff+      Prairie.Fold+      Prairie.Traverse       Prairie.TH    build-depends:@@ -87,4 +89,5 @@     , prairie     , aeson     , lens+    , hspec   default-language: Haskell2010
src/Prairie.hs view
@@ -5,10 +5,14 @@     ( module Prairie.Class     , module Prairie.Update     , module Prairie.Diff+    , module Prairie.Fold+    , module Prairie.Traverse     , module Prairie.TH     ) where  import Prairie.Class-import Prairie.Update import Prairie.Diff+import Prairie.Fold import Prairie.TH+import Prairie.Traverse+import Prairie.Update
+ src/Prairie/Fold.hs view
@@ -0,0 +1,108 @@+-- | This module shows you how to fold a 'Record'.+--+-- These utilities are based on 'recordToFieldList', which converts+-- a 'Record' into a 'SomeFieldWithValue'. Then, 'foldRecord' unpacks that+-- GADT for you, which allows you to know the type of the field and combine+-- them.+--+-- @since 0.0.3.0+module Prairie.Fold where++import Control.Monad (foldM)+import Prairie.Class+import Data.List (foldl')++-- | A datatype containing a 'Field' along with a value for that field.+--+-- @since 0.0.3.0+data SomeFieldWithValue rec where+    SomeFieldWithValue :: Field rec a -> a -> SomeFieldWithValue rec++-- | Convert a 'Record' into a list of the records fields paired with the+-- value for that record.+--+-- @since 0.0.3.0+recordToFieldList :: forall rec. (Record rec) => rec -> [SomeFieldWithValue rec]+recordToFieldList rec =+    fmap+        (\(SomeField field) ->+            SomeFieldWithValue field (getRecordField field rec)+        )+        (allFields @rec)++-- | Fold over the fields of a record to produce a final result.+--+-- The function parameter accepts the @'Field' rec ty@, a value in the+-- record, and the accumulator. Example:+--+-- @+-- 'foldRecord'+--      (\\ val acc field ->+--          case field of+--              UserName ->+--                  length val + acc+--              UserAge ->+--                  val + acc+--      )+--      0+--      User { userName = \"Matt\", userAge = 35 }+-- @+--+-- The paramater list is given to enable 'LambdaCase' nicety:+--+-- @+-- 'foldRecord'+--      (\\val acc -> \\case+--          UserName ->+--              length val + acc+--          UserAge ->+--              val + acc+--      )+-- @+--+-- @since 0.0.3.0+foldRecord+    :: forall rec r+     . (Record rec)+    => (forall ty. ty -> r -> Field rec ty -> r)+    -> r+    -> rec+    -> r+foldRecord k init rec =+    foldl'+        (\acc (SomeFieldWithValue field value) ->+            k value acc field+        )+        init+        (recordToFieldList rec)++-- | Fold over a 'Record' with a monadic action.+--+-- @since 0.0.3.0+foldMRecord+    :: forall rec m r+     . (Record rec, Monad m)+    => (forall ty. ty -> r -> Field rec ty -> m r)+    -> r+    -> rec+    -> m r+foldMRecord k init rec =+    foldM+        (\acc (SomeFieldWithValue field value) ->+            k value acc field+        )+        init+        (recordToFieldList rec)++-- | Convert each field of a 'Record' into a monoidal value and combine+-- them together using 'mappend'.+--+-- @since 0.0.3.0+foldMapRecord+    :: forall rec m+    . (Record rec, Monoid m)+    => (forall ty. ty -> Field rec ty -> m)+    -> rec+    -> m+foldMapRecord k rec =+    foldMap (\(SomeFieldWithValue f v) -> k v f) (recordToFieldList rec)
+ src/Prairie/Traverse.hs view
@@ -0,0 +1,66 @@+-- | This module shows how to 'traverse' over a 'Record', allowing you to+-- perform side-effects for each field and return a transformed value.+--+-- @since 0.0.3.0+module Prairie.Traverse where++import Data.List (foldl')+import Prairie.Class+import Control.Applicative (liftA2)+import Prairie.Fold++-- | Apply an effectful function over each field of a 'Record', producing+-- a new 'Record' in the process.+--+-- This example use increments a @User@s age and requests a new name.+--+-- @+-- happyBirthday :: User -> IO User+-- happyBirthday =+--     traverseRecord+--          (\\val field ->+--              case field of+--                  UserName -> do+--                      putStrLn $ "Current name is: " <> val+--                      putStrLn "Please input a new name: "+--                      getLine+--                  UserAge -> do+--                      putStrLn $ "Current age is " <> show val+--                      pure (val + 1)+--          )+-- @+--+-- If you only want to target a single field, you can use a wildcard match+-- and `pure`. This example also uses @LambdaCase@.+--+-- @+-- nameAtLeastOneCharacter :: User -> 'Maybe' User+-- nameAtLeastOneCharacter =+--      'traverseRecord'+--          (\val ->+--              \case+--                  UserName -> do+--                      'guard' ('length' val >= 1)+--                      'pure' val+--                  _ ->+--                      'pure' val+--          )+-- @+--+-- @since 0.0.3.0+traverseRecord+    :: forall rec f. (Record rec, Applicative f)+    => (forall ty. ty -> Field rec ty -> f ty)+    -> rec+    -> f rec+traverseRecord f init =+    foldl' k (pure init) (recordToFieldList init)+  where+    f' :: Field rec ty -> ty -> f (SomeFieldWithValue rec)+    f' field val =+        SomeFieldWithValue field <$> f val field+    k frec (SomeFieldWithValue field val) =+        liftA2+            (\(SomeFieldWithValue field' val') rec -> setRecordField field' val' rec)+            (f' field val)+            frec
test/Spec.hs view
@@ -1,23 +1,40 @@-{-# language PolyKinds, LambdaCase, TypeApplications, RankNTypes, StandaloneDeriving, ConstraintKinds, TemplateHaskell, DataKinds, OverloadedStrings, FlexibleInstances, MultiParamTypeClasses, UndecidableInstances, TypeFamilies, GADTs #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}  {-# options_ghc -Wall #-}+ module Main where  import Prairie -import Data.Aeson-import Control.Monad import Control.Lens+import Control.Monad+import Data.Aeson import Data.Kind (Type)+import Data.Monoid+import Test.Hspec  data User = User { name :: String, age :: Int }-  deriving Eq+  deriving (Show, Eq)  mkRecord ''User  deriving instance Eq (Field User a)+deriving instance Show (Field User a) -example = User "Alice" 30+exampleUser = User "Alice" 30  data T a = T { x :: a, y :: Int } @@ -48,44 +65,135 @@     FieldLens (Field (t x) x) y f = LensLike f (t x) (t y) x y     FieldLens (Field (t x) y) y f = LensLike f (t x) (t x) y y -assert :: String -> Bool -> IO ()-assert message success = unless success (error message)- main :: IO ()-main = do-    assert "getField" $ getRecordField UserName example == "Alice"-    assert "setField" $ setRecordField UserAge 32 example == User "Alice" 32-    assert "label" $ recordFieldLabel UserAge == "age"-    assert "label" $ recordFieldLabel UserName == "name"+main = hspec $ do+    describe "Prairie" $ do+        it "getField" $ do+            getRecordField UserName exampleUser `shouldBe` "Alice"+        it "setField" $ do+            setRecordField UserAge 32 exampleUser `shouldBe` User { name = "Alice", age = 32 }+        it "label" $ do+            recordFieldLabel UserAge `shouldBe` "age"+        it "label" $ do+            recordFieldLabel UserName `shouldBe` "name" -    let t :: T Int-        t = T 3 2+        let t :: T Int+            t = T 3 2 -        t' :: T Char-        t' = t & polyLens TX .~ 'a'+            t' :: T Char+            t' = t & polyLens TX .~ 'a' -    assert "update json" $-        encode (diffRecord example (setRecordField UserName "Bob" example))-        ==-        "[{\"field\":\"name\",\"value\":\"Bob\"}]"+        it "update json" $+            encode (diffRecord exampleUser (setRecordField UserName "Bob" exampleUser))+            `shouldBe`+            "[{\"field\":\"name\",\"value\":\"Bob\"}]" -    assert "decode update" $-      decode "[{\"field\":\"name\",\"value\":\"Bob\"}]"-      ==-      Just [SetField UserName "Bob"]+        it "decode update" $+          decode "[{\"field\":\"name\",\"value\":\"Bob\"}]"+          `shouldBe`+          Just [SetField UserName "Bob"] -    user' <--      tabulateRecordA $ \case-          UserName ->-              print 10 >> pure "Matt"-          UserAge ->-              print 20 >> pure 33-    assert "tabulateRecordA" $-        user'-        ==-        User-            { name = "Matt"-            , age = 33-            }+        it "tabulateRecordA" $ do+            user' <-+              tabulateRecordA $ \case+                  UserName ->+                      print 10 >> pure "Matt"+                  UserAge ->+                      print 20 >> pure 33+            user'+                `shouldBe`+                User+                    { name = "Matt"+                    , age = 33+                    } +        describe "Fold" $ do+            describe "foldRecord" $ do+                it "can count the fields" $ do+                    foldRecord (\_val acc _field -> acc + 1) 0 exampleUser+                        `shouldBe`+                            2+                it "can distinguish fields" $ do+                    foldRecord+                        (\val acc field ->+                            case field of+                                UserName ->+                                    length val + acc+                                UserAge ->+                                    val + acc+                        )+                        (0 :: Int)+                        exampleUser+                        `shouldBe`+                            35 +            describe "foldMapRecord" $ do+                it "can count the fields" $ do+                    foldMapRecord (\_ _ -> Sum 1) exampleUser+                        `shouldBe`+                            Sum 2+                it "can combine strings" $ do+                    foldMapRecord+                        (\val ->+                            \case+                                UserName -> val+                                UserAge -> show val+                        )+                        exampleUser+                        `shouldBe`+                            ("Alice30" :: String)++        describe "Traverse" $ do+            it "can validate a record" $ do+                let validateField :: ty -> Field User ty -> Maybe ty+                    validateField val =+                        \case+                            UserName -> do+                                guard (length val >= 1)+                                pure val+                            UserAge -> do+                                guard (val >= 18)+                                pure val+                traverseRecord validateField exampleUser+                    `shouldBe`+                        Just User { name = "Alice", age = 30 }++                traverseRecord validateField User { name = "", age = 1 }+                    `shouldBe`+                        Nothing+++            it "can do either" $ do+                let validateField :: ty -> Field User ty -> Either String ty+                    validateField val =+                        \case+                            UserName -> do+                                if length val >= 1+                                    then pure val+                                    else Left "Name must be at least one character"+                            UserAge -> do+                                if val >= 18+                                    then pure val+                                    else Left "Age must be at least 18"++                traverseRecord validateField exampleUser+                    `shouldBe`+                        Right User { name = "Alice", age = 30 }++                traverseRecord validateField User { name = "", age = 1 }+                    `shouldBe`+                        Left "Age must be at least 18"++            it "can target one field" $ do+                traverseRecord+                    (\val ->+                        \case+                            UserName -> do+                                guard (length val >= 1)+                                pure val+                            _ ->+                                pure val+                    )+                    User { name = "", age = 30 }+                    `shouldBe`+                        Nothing