packages feed

typson-esqueleto (empty) → 0.1.0.0

raw patch · 8 files changed

+361/−0 lines, 8 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, esqueleto, exceptions, hedgehog, microlens, persistent, persistent-postgresql, persistent-template, postgresql-simple, tasty, tasty-hedgehog, tasty-hunit, test-fixture, text, typson-core, typson-esqueleto

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for typson-esqueleto++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Aaron Allen (c) 2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Aaron Allen nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,1 @@+# typson-esqueleto
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Typson/Esqueleto.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE PolyKinds #-}+--------------------------------------------------------------------------------+-- |+-- Module      : Typson.Esqueleto+-- Description : Provides the Esqueleto integration+-- Copyright   : (c) Aaron Allen, 2020+-- Maintainer  : Aaron Allen <aaronallen8455@gmail.com>+-- License     : BSD-style (see the file LICENSE)+-- Stability   : experimental+-- Portability : non-portable+--+--------------------------------------------------------------------------------+module Typson.Esqueleto+  ( jsonPath+  , NullableJSONB(..)+  , PostgreSqlJSON+  ) where++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as Aeson+import           Data.Bifunctor (first)+import           Data.List (foldl')+import qualified Data.List.NonEmpty as NE+import qualified Data.Text as T+import qualified Database.Esqueleto as E+import qualified Database.Esqueleto.PostgreSQL.JSON as E+import qualified Database.Esqueleto.Internal.Internal as E+import           GHC.Generics (Generic)++import           Typson++-- | Use a type-safe JSON path as part of a query.+--+-- @+-- select . from $ \entity ->+--   pure . jsonPath (Proxy @("foo" :-> "bar")) fieldSchemaJ+--     $ entity ^. Field+-- @+jsonPath :: ( TypeAtPath o tree path ~ field+            , ReflectPath path+            , PostgreSqlJSON json+            )+         => proxy (path :: k) -- ^ A path proxy+         -> ObjectTree tree o -- ^ Typson schema+         -> E.SqlExpr (E.Value (json o)) -- ^ Column selector+         -> E.SqlExpr (E.Value (NullableJSONB field))+jsonPath path _ input =+  case reflectPath path of+    p NE.:| ps -> foldl' buildPath (buildPath input p) ps+  where+    buildPath p (Key k) = p `arrOp` E.val k+    buildPath p (Idx i) = p `arrOp` E.val (fromIntegral i :: Int)+    arrOp = E.unsafeSqlBinOp " -> "++-- | Treats SQL @NULL@ as a JSON @null@+newtype NullableJSONB a =+  NullableJSONB+    { unNullableJSONB :: a+    } deriving ( Generic+               , Aeson.FromJSON+               , Aeson.ToJSON+               , Foldable+               , Functor+               , Eq+               , Ord+               , Read+               , Show+               , Traversable+               )+      deriving E.PersistFieldSql via (E.JSONB a)++instance (Aeson.FromJSON a, Aeson.ToJSON a)+      => E.PersistField (NullableJSONB a) where+  toPersistValue = E.toPersistValue . E.JSONB . unNullableJSONB+  fromPersistValue pVal = fmap NullableJSONB $ case pVal of+      E.PersistNull -> first T.pack+                     $ Aeson.parseEither Aeson.parseJSON Aeson.Null+      _ -> E.unJSONB <$> E.fromPersistValue pVal++-- | Members of this class are type constructors used to respresent+-- Postgres JSON columns.+class PostgreSqlJSON (json :: * -> *)+instance PostgreSqlJSON NullableJSONB+instance PostgreSqlJSON E.JSONB
+ test/Spec.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}++import           Control.Monad (void)+import           Control.Monad.Catch (handleAll)+import qualified Data.ByteString.Char8 as BS+import           Data.List (sort)+import qualified Database.Esqueleto as E+import qualified Database.Esqueleto.PostgreSQL.JSON as E+import qualified Database.Persist.Postgresql as P+import qualified Database.PostgreSQL.Simple as Pg+import qualified Hedgehog.Gen as HH+import qualified Hedgehog.Range as HH+import           System.Environment (lookupEnv)+import           Test.Tasty+import           Test.Tasty.HUnit++import           Typson.Esqueleto+import           Typson.Test.Esqueleto.DbSchema (EsqueletoEntity(..), EntityField(EsqueletoEntityGraph), migrateAll)+import           Typson.Test.Generators+import           Typson.Test.Types++main :: IO ()+main = defaultMain esqueletoTestTree++esqueletoTestTree :: TestTree+esqueletoTestTree = withRunDb $ \runDb ->+  testGroup "Esqueleto Tests"+  [ testCase "JSON Queries" $ do+      graphs <- HH.sample (HH.list (HH.singleton 100) bazGen)+      runDb (insertData graphs)++      r1 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath basicPath1 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a1 = E.Value . NullableJSONB . basicPath1Getter <$> graphs+      assertEqual "Basic Path 1" (sort r1) (sort a1)++      r2 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath basicPath2 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a2 = E.Value . NullableJSONB . basicPath2Getter <$> graphs+      assertEqual "Basic Path 2" (sort r2) (sort a2)++      r3 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath basicPath3 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a3 = E.Value . NullableJSONB . basicPath3Getter <$> graphs+      assertEqual "Basic Path 3" (sort r3) (sort a3)++      r4 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath optionalPath1 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a4 = E.Value . NullableJSONB . optionalPath1Getter <$> graphs+      assertEqual "Optional Path 1" (sort r4) (sort a4)++      r5 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath optionalPath2 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a5 = E.Value . NullableJSONB . optionalPath2Getter <$> graphs+      assertEqual "Optional Path 2" (sort r5) (sort a5)++      r6 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath optionalPath3 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a6 = E.Value . NullableJSONB . optionalPath3Getter <$> graphs+      assertEqual "Optional Path 3" (sort r6) (sort a6)++      r7 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath listIdxPath1 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a7 = E.Value . NullableJSONB . listIdxPath1Getter <$> graphs+      assertEqual "List Idx Path 1" (sort r7) (sort a7)++      r8 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath listIdxPath2 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a8 = E.Value . NullableJSONB . listIdxPath2Getter <$> graphs+      assertEqual "List Idx Path 2" (sort r8) (sort a8)++      r9 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath listIdxPath3 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a9 = E.Value . NullableJSONB . listIdxPath3Getter <$> graphs+      assertEqual "List Idx Path 3" (sort r9) (sort a9)++      r10 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath unionPath1 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a10 = E.Value . NullableJSONB . unionPath1Getter <$> graphs+      assertEqual "Union Path 1" (sort r10) (sort a10)++      r11 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath unionPath2 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a11 = E.Value . NullableJSONB . unionPath2Getter <$> graphs+      assertEqual "Union Path 2" (sort r11) (sort a11)++      r12 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath textMapPath1 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a12 = E.Value . NullableJSONB . textMapPath1Getter <$> graphs+      assertEqual "Text Map Path 1" (sort r12) (sort a12)++      r13 <- runDb . E.select . E.from $ \entity ->+              pure . jsonPath textMapPath2 bazJ+                $ entity E.^. EsqueletoEntityGraph+      let a13 = E.Value . NullableJSONB . textMapPath2Getter <$> graphs+      assertEqual "Text Map Path 2" (sort r13) (sort a13)+  ]++type DbRunner = forall b. P.SqlPersistT IO b -> IO b++withRunDb :: (DbRunner -> TestTree) -> TestTree+withRunDb mkTree = withDb $ \ioBackend -> mkTree $ \action -> do+  backend <- ioBackend+  P.runSqlConn action backend++withDb :: (IO P.SqlBackend -> TestTree) -> TestTree+withDb = withResource connectToDb P.connClose++connectToDb :: IO P.SqlBackend+connectToDb = do+  Just connString <- lookupEnv "CONN_STRING"+  conn <- Pg.connectPostgreSQL $ BS.pack connString+  backend <- P.openSimpleConn (\_ _ _ _ -> pure ()) conn++  -- reset the table+  _ <- handleAll (const $ pure 0) $ Pg.execute_ conn "DROP TABLE \"esqueleto_entity\""++  P.runSqlConn (P.runMigration migrateAll) backend++  pure backend++insertData :: [Baz] -> P.SqlPersistT IO ()+insertData graphs =+  void $ P.insertMany (EsqueletoEntity . E.JSONB <$> graphs)
+ test/Typson/Test/Esqueleto/DbSchema.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE MultiParamTypeClasses #-}+module Typson.Test.Esqueleto.DbSchema where++import qualified Database.Esqueleto.PostgreSQL.JSON as E+import qualified Database.Persist.TH as P++import           Typson.Test.Types (Baz)++P.share [P.mkPersist P.sqlSettings, P.mkMigrate "migrateAll"] [P.persistLowerCase|+EsqueletoEntity+  graph (E.JSONB Baz)+  deriving Show Eq+|]
+ typson-esqueleto.cabal view
@@ -0,0 +1,75 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 7a0e7aa5be266845f2d78a10d41cc445864d4f220224bf926e92d1df3b1a119c++name:           typson-esqueleto+version:        0.1.0.0+synopsis:       Typson Esqueleto Integration+description:    Please see the README on GitHub at <https://github.com/aaronallen8455/typson#readme>+category:       Database+homepage:       https://github.com/aaronallen8455/typson#readme+bug-reports:    https://github.com/aaronallen8455/typson/issues+author:         Aaron Allen+maintainer:     aaronallen8455@gmail.com+copyright:      2020 Aaron Allen+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    ChangeLog.md++source-repository head+  type: git+  location: https://github.com/aaronallen8455/typson++library+  exposed-modules:+      Typson.Esqueleto+  other-modules:+      Paths_typson_esqueleto+  hs-source-dirs:+      src+  build-depends:+      aeson+    , base >=4.7 && <5+    , esqueleto+    , persistent+    , persistent-template+    , text+    , typson-core+  default-language: Haskell2010++test-suite typson-esqueleto-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Typson.Test.Esqueleto.DbSchema+      Paths_typson_esqueleto+  hs-source-dirs:+      test+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson+    , base >=4.7 && <5+    , bytestring+    , esqueleto+    , exceptions+    , hedgehog+    , microlens+    , persistent+    , persistent-postgresql+    , persistent-template+    , postgresql-simple+    , tasty+    , tasty-hedgehog+    , tasty-hunit+    , test-fixture+    , text+    , typson-core+    , typson-esqueleto+  default-language: Haskell2010