packages feed

typson-core 0.1.0.0 → 0.1.0.1

raw patch · 2 files changed

+347/−36 lines, 2 filesdep ~aesondep ~containersdep ~profunctorsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, containers, profunctors, text, unordered-containers, vector

API changes (from Hackage documentation)

Files

+ README.md view
@@ -0,0 +1,303 @@+# Typson++## Introduction++Typson uses type level programming to enable writing JSON PostgreSQL queries+that are type safe. There are integrations available for the `esqueleto`,+`beam`, and `selda` libraries.++The ability to store and query arbitrary JSON in a database can be very+beneficial, however, writing these queries with raw SQL is particularly bug+prone because the correctness of the path keys, the nullability of fields, and+the resulting type must all be accounted for. Typson solves these issues by+describing the JSON structure of the data at the type level so that any errors+will manifest as compile time exceptions rather than runtime bugs.++Typson allows you to define a single schema value which then functions as+both an encoder and decoder for JSON representations. The actual encoding and+decoding is handled by the `aeson` library. This same schema is also what's used+to provide type safety for JSON SQL queries.++## Example++Now for a rather contrived demonstration of how the library works and what you+can do with it.++### Defining our types++Let's say we want to store information about various life forms. We have+different attributes depending on if the subject is a plant or an animal, which+we model with the `Classifier` sum type.++```hs+data LifeForm =+  LifeForm+    { classifier :: Classifier+    , name :: T.Text+    } deriving (Eq, Show)++data Classifier+  = Flora Plant+  | Fauna Animal+  deriving (Eq, Show)++data Plant =+  Plant+    { isAquatic :: Bool+    , bearsFruit :: Bool+    } deriving (Eq, Show)++data Animal =+  Animal+    { favoriteFoods :: [T.Text]+    , isGoodPet :: Bool+    } deriving (Eq, Show)+```++### Defining schemas++Now we'll define the schemas that Typson will use to type check our SQL queries.++```hs+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE PartialTypeSignatures #-}++import Typson++plantJ :: JsonSchema _ Plant+plantJ = object "Plant" $+  Plant+    <<$> field (key @"isAquatic") isAquatic prim+    <<*> field (key @"bearsFruit") bearsFruit prim++animalJ :: JsonSchema _ Animal+animalJ = object "Animal" $+  Animal+    <<$> field (key @"favoriteFoods") favoriteFoods (list prim)+    <<*> field (key @"isGoodPet") isGoodPet prim+```++A few things to note here:+- `JsonSchema` is just a rank-N type synonym for a set of constraints. It's+first argument is the type level data structure that defines the JSON structure.+It's not very useful or readable to have this parameter written out in the code+and the compiler will generate it for you, so it is recommended to use the+`PartialTypeSignatures` extension to hide it. The last argument is the type that+is being described.+- Record types are described using the `object` function and the fields are+defined using syntax that is akin to the standard Applicative form but with+`<<$>` instead of `<$>` and `<<*>` instead of `<*>`. This is to support the+type-level machinery, which the normal `Applicative` type class cannot do.+- There are several combinators for defining fields. `field` denotes a required+attribute which translates to a non-null field in the JSON.+- `list` wraps a schema for some type `a`, turning it into a schema for `[a]`.+This will allow us to query for specific indices of the list.+- The first argument of the field combinators is the key to be used in the JSON.+It is supplied using `key` which is just a synonym for `Proxy`. A type error+occurrs if a key appears more than once in the same object.+- The second argument is the field accesor of the record.+- The final argument is the schema associated with the type of the field. If the+field's type is some sort of primative that has predefined `ToJSON` and+`FromJSON` instances, then the `prim` schema should be used.++Now for the remaining two schemas. Of special interest is the schema for the+`Classifier` sum type.++```hs+classifierJ :: JsonSchema _ Classifier+classifierJ = union "Classifier" $+  classifierTags+    <<$> tag (key @"flora") Flora plantJ+    <<*> tag (key @"fauna") Fauna animalJ+  where+    classifierTags withPlant withAnimal = \case+      Flora plant  -> withPlant plant+      Fauna animal -> withAnimal animal++lifeFormJ :: JsonSchema _ LifeForm+lifeFormJ = object "LifeForm" $+  LifeForm+    <<$> field (key @"classifier") classifier classifierJ+    <<*> field (key @"name") name prim+```++Here we see the somewhat peculiar `union` function which is how schemas for sum+types are defined. We define a function that takes handlers for the contents of+each of the sum's constructors which then pattern matches on the sum and applies+the corresponding handlers to the pattern matched values. The function is+applied over `tag` definitions, one for each branch of the sum, using the same+specialized `Applicative` syntax as before.++Also noteworthy is that we are now referencing other schemas in the last+argument of some of these `field` and `tag` definitions instead of `prim`.++### Writing `aeson` instances++You should always define `FromJSON` and `ToJSON` instances using the Typson+schema for a given type, which is as simple as using the `decodeObject` and+`encodeObject` functions from `typson-core`:++```hs+import Data.Aeson++instance FromJSON LifeForm where+  parseJSON = decodeObject lifeFormJ++instance ToJSON LifeForm where+  toJSON = encodeObject lifeFormJ++instance FromJSON Plant where+  parseJSON = decodeObject plantJ++instance ToJSON Plant where+  toJSON = encodeObject plantJ++instance FromJSON Animal where+  parseJSON = decodeObject animalJ++instance ToJSON Animal where+  toJSON = encodeObject animalJ++instance FromJSON Classifier where+  parseJSON = decodeObject classifierJ++instance ToJSON Classifier where+  toJSON = encodeObject classifierJ+```++### Setting up the database table++We'll be using the `esqueleto` library, which sits on top of `persistent`, to+demonstrate some database interactions. Integrations are also available for the+`beam` and `selda` libraries.++First we'll define a table that has a single `JSONB` column to hold the JSON of+our `LifeForm` type (in addition to the implicit 'id' column).++```hs+import           Database.Esqueleto+import qualified Database.Persist.Postgresql as P+import qualified Database.Persist.TH as P++P.share [P.mkPersist P.sqlSettings, P.mkMigrate "migrateAll"] [P.persistLowerCase|+LifeFormEntity+  object (JSONB LifeForm)+  deriving Show Eq+|]++-- sample data+lifeFormEntities :: [LifeFormEntity]+lifeFormEntities+  = [ LifeFormEntity $ JSONB lion+    , LifeFormEntity $ JSONB dog+    , LifeFormEntity $ JSONB kelp+    , LifeFormEntity $ JSONB bananaTree+    ]++main :: IO ()+main = do+  Just connString <- lookupEnv "CONN_STRING"+  runStderrLoggingT . P.withPostgresqlConn (BS.pack connString) $ \be ->+    (`P.runSqlConn` be) $ do+      P.runMigration migrateAll++      void $ P.insertMany lifeFormEntities+```++The `main` function simply connects to the database, creates the table if it+doesn't exist, and populates it with some sample data (omitted here).++### Querying against a JSON column++Now that we've created our table and populated it with the sample data, we're+ready to write some type safe JSON queries!++```hs+import Data.Proxy+import Typson.Esqueleto++-- Select the names of all life forms.+selectNames :: P.SqlPersistT (LoggingT IO) [T.Text]+selectNames+  = fmap coerce -- unwrap newtypes+  . select . from+  $ \e ->+      pure . jsonPath (Proxy @"name") lifeFormJ+        $ e ^. LifeFormEntityObject++-- Select just the animals+selectAnimals :: P.SqlPersistT (LoggingT IO) [Animal]+selectAnimals+  = fmap (mapMaybe coerce)+  . select . from+  $ \e -> do+      pure . jsonPath (Proxy @("classifier" :-> "fauna")) lifeFormJ+        $ e ^. LifeFormEntityObject+```++The key component here is the `jsonPath` function, which takes these arguments:+- A `Proxy` for the path to the field that we want to select. The `:->` type+operator is used to construct a path from multiple keys, akin to how paths are+written in raw SQL queries using `->`.+- Our schema value+- The `esqueleto` identifier for the JSON column.++If we had typo'ed the keys or constructed an invalid path, we'd get a helpful+type error about it, such as++```+/code/example/src/Main.hs:88:5: error:+    • JSON key not present in Classifier: "fawna"+```++Typson is able to calculate what the resulting type should be given a path into+the object, so our query is well typed!++Here are some more example queries:++```hs+-- Selects all animals that make good pets+selectGoodPets :: P.SqlPersistT (LoggingT IO) [P.Entity LifeFormEntity]+selectGoodPets+  = select . from+  $ \e -> do+      let goodPetPath =+            jsonPath (Proxy @("classifier" :-> "fauna" :-> "isGoodPet")) lifeFormJ++      where_ $ goodPetPath (e ^. LifeFormEntityObject)+                 ==. val (NullableJSONB (Just True))+      pure e++-- Selects the names of all the aquatic plants+selectAquaticPlantNames :: P.SqlPersistT (LoggingT IO) [T.Text]+selectAquaticPlantNames+  = fmap coerce -- unwrap newtypes+  . select . from+  $ \e -> do+      let isAquaticPath =+            jsonPath (Proxy @("classifier" :-> "flora" :-> "isAquatic")) lifeFormJ+      where_ $ isAquaticPath (e ^. LifeFormEntityObject)+                 ==. val (NullableJSONB (Just True))+      pure $ jsonPath (Proxy @("name")) lifeFormJ (e ^. LifeFormEntityObject)++-- Select pairs of names to the 1st item in the list of favorite foods if the+-- entity is an animal.+selectFavoriteFood :: P.SqlPersistT (LoggingT IO) [(T.Text, Maybe T.Text)]+selectFavoriteFood+  = fmap coerce -- unwrap newtypes+  . select . from+  $ \e -> do+      let o = e ^. LifeFormEntityObject+          name' = jsonPath (Proxy @"name") lifeFormJ o+          favoriteFood =+            jsonPath (Proxy @("classifier" :-> "fauna" :-> "favoriteFoods" :-> 0)) lifeFormJ o+      pure (name', favoriteFood)+```++Here we have `where` clauses that reference specific JSON fields.+There is also an instance of using a numeric literal as part of the path to+query a particular index of an array field.++In conclusion, as long as the JSON in your database was produced using the same+schema that's being used for querying, then you can rest assured that your JSON+queries won't result in runtime bugs!
typson-core.cabal view
@@ -1,41 +1,49 @@-cabal-version:      1.12-name:               typson-core-version:            0.1.0.0-license:            BSD3-license-file:       LICENSE-copyright:          2020 Aaron Allen-maintainer:         aaronallen8455@gmail.com-author:             Aaron Allen-homepage:           https://github.com/aaronallen8455/typson#readme-bug-reports:        https://github.com/aaronallen8455/typson/issues-synopsis:           Type-safe PostgreSQL JSON Querying-description:-    Please see the README on GitHub at <https://github.com/aaronallen8455/typson#readme>+cabal-version: 1.12 -category:           Database-build-type:         Simple-extra-source-files: ChangeLog.md+-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 14c0e54e6bead0e054aefaa30c7e83d9ad1d686ae2f8cbd5b6cb850e0833bab0 +name:           typson-core+version:        0.1.0.1+synopsis:       Type-safe PostgreSQL JSON Querying+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:+    ChangeLog.md+    README.md+ source-repository head-    type:     git-    location: https://github.com/aaronallen8455/typson+  type: git+  location: https://github.com/aaronallen8455/typson  library-    exposed-modules:-        Typson-        Typson.JsonTree-        Typson.Optics-        Typson.Pathing--    hs-source-dirs:   src-    other-modules:    Paths_typson_core-    default-language: Haskell2010-    ghc-options:      -Wall-    build-depends:-        aeson >=1.4.7.1 && <1.5,-        base >=4.7 && <5,-        containers >=0.6.2.1 && <0.7,-        profunctors >=5.5.2 && <5.6,-        text >=1.2.4.0 && <1.3,-        unordered-containers >=0.2.10.0 && <0.3,-        vector >=0.12.1.2 && <0.13+  exposed-modules:+      Typson+      Typson.JsonTree+      Typson.Optics+      Typson.Pathing+  other-modules:+      Paths_typson_core+  hs-source-dirs:+      src+  ghc-options: -Wall+  build-depends:+      aeson+    , base >=4.7 && <5+    , containers+    , profunctors+    , text+    , unordered-containers+    , vector+  default-language: Haskell2010