From 6a7350e66591456e22996feda409edcce7ab19b0 Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Thu, 14 Dec 2023 15:15:50 -0600 Subject: [PATCH] feat: record builder --- bun.lockb | Bin 53117 -> 53484 bytes package.json | 2 +- spago.yaml | 4 +++ src/Control.Monad.Postgres.purs | 9 ------ src/Data.Postgres.Extract.Record.purs | 45 ++++++++++++++++++++++++++ src/Data.Postgres.Types.purs | 16 +++++++++ test/Spec.Data.Postgres.Record.purs | 45 ++++++++++++++++++++++++++ test/Test.Main.purs | 13 ++++++++ 8 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 src/Data.Postgres.Extract.Record.purs create mode 100644 src/Data.Postgres.Types.purs create mode 100644 test/Spec.Data.Postgres.Record.purs create mode 100644 test/Test.Main.purs diff --git a/bun.lockb b/bun.lockb index c42d05ceb16556e06bd80e7e360bf13128ae05e8..8c74b909d91a4f7802e2f51b0430e67c091e3b6a 100755 GIT binary patch delta 8102 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 delta 7980 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 HasPostgresT m a -> m a runHasPostgres conn t = (unwrap $ unwrap t) conn - -newtype RawRow = RawRow (Array Foreign) - -derive instance Newtype RawRow _ -instance Pg.FromSQLRow RawRow where - fromSQLRow = pure <<< wrap - -instance Pg.ToSQLRow RawRow where - toSQLRow = unwrap diff --git a/src/Data.Postgres.Extract.Record.purs b/src/Data.Postgres.Extract.Record.purs new file mode 100644 index 0000000..9812629 --- /dev/null +++ b/src/Data.Postgres.Extract.Record.purs @@ -0,0 +1,45 @@ +module Data.Postgres.Extract.Record where + +import Prelude + +import Control.Monad.Error.Class (liftMaybe) +import Data.Array as Array +import Data.Either (Either) +import Data.Maybe (fromMaybe) +import Data.Postgres.Types (RawRow(..)) +import Database.PostgreSQL (class FromSQLValue, fromSQLValue) +import Foreign (Foreign) +import Prim.Row (class Cons, class Lacks) +import Record as Record +import Type.Function (type ($)) +import Type.Prelude (class IsSymbol, Proxy(..)) + +data ExtractRecord a = ExtractRecord (Array Foreign) a + +extract :: forall a. (ExtractRecord {} -> Either String $ ExtractRecord a) -> RawRow -> Either String a +extract f = map finish <<< f <<< start + +finish :: forall a. ExtractRecord a -> a +finish (ExtractRecord _ a) = a + +start :: RawRow -> ExtractRecord {} +start (RawRow r) = ExtractRecord r {} + +skip :: forall a. ExtractRecord a -> ExtractRecord a +skip (ExtractRecord r a) = ExtractRecord (fromMaybe [] $ Array.tail r) a + +field + :: forall a b @k @v + . Show (Record a) + => Lacks k a + => Cons k v a b + => IsSymbol k + => FromSQLValue v + => ExtractRecord $ Record a + -> Either String $ ExtractRecord $ Record b +field (ExtractRecord row r) = do + let + eempty = "no more values to consume, built: " <> show r + col <- liftMaybe eempty $ Array.head row + val <- fromSQLValue col + pure $ skip $ ExtractRecord row $ Record.insert (Proxy @k) val r diff --git a/src/Data.Postgres.Types.purs b/src/Data.Postgres.Types.purs new file mode 100644 index 0000000..5330f64 --- /dev/null +++ b/src/Data.Postgres.Types.purs @@ -0,0 +1,16 @@ +module Data.Postgres.Types where + +import Prelude + +import Data.Newtype (class Newtype, unwrap, wrap) +import Database.PostgreSQL (class FromSQLRow, class ToSQLRow) +import Foreign (Foreign) + +newtype RawRow = RawRow (Array Foreign) + +derive instance Newtype RawRow _ +instance FromSQLRow RawRow where + fromSQLRow = pure <<< wrap + +instance ToSQLRow RawRow where + toSQLRow = unwrap diff --git a/test/Spec.Data.Postgres.Record.purs b/test/Spec.Data.Postgres.Record.purs new file mode 100644 index 0000000..f4911f6 --- /dev/null +++ b/test/Spec.Data.Postgres.Record.purs @@ -0,0 +1,45 @@ +module Spec.Data.Postgres.Record where + +import Prelude + +import Control.Monad.Error.Class (liftEither, throwError) +import Data.Bifunctor (lmap) +import Data.Either (isRight) +import Data.Postgres.Extract.Record as Rec +import Data.Postgres.Types (RawRow(..)) +import Effect.Exception (error) +import Foreign (unsafeToForeign) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) + +spec :: Spec Unit +spec = + describe "Data.Postgres.Record" do + it "empty case" do + let raw = RawRow [ unsafeToForeign 1 ] + (Rec.finish $ Rec.start raw) `shouldEqual` {} + it "fails when type incorrect" do + let + raw = RawRow [ unsafeToForeign 12 ] + actual = map Rec.finish $ Rec.field @"foo" @String $ Rec.start raw + when (isRight actual) $ throwError $ error "should have failed" + it "succeeds" do + let + raw = + RawRow + [ unsafeToForeign 12 + , unsafeToForeign "hello" + , unsafeToForeign "SKIP" + , unsafeToForeign [ 1, 2, 3 ] + ] + actual <- + liftEither + $ lmap error + $ Rec.extract + ( Rec.field @"num" + >=> Rec.field @"str" + >=> (pure <<< Rec.skip) + >=> Rec.field @"arr" + ) + $ raw + actual `shouldEqual` { num: 12, str: "hello", arr: [ 1, 2, 3 ] } diff --git a/test/Test.Main.purs b/test/Test.Main.purs new file mode 100644 index 0000000..e5186b5 --- /dev/null +++ b/test/Test.Main.purs @@ -0,0 +1,13 @@ +module Test.Main where + +import Prelude + +import Effect (Effect) +import Effect.Aff (launchAff_) +import Spec.Data.Postgres.Record as Spec.Data.Postgres.Record +import Test.Spec.Reporter (consoleReporter) +import Test.Spec.Runner (runSpec) + +main :: Effect Unit +main = launchAff_ $ runSpec [ consoleReporter ] do + Spec.Data.Postgres.Record.spec