{-# LANGUAGE TemplateHaskell #-} {-| Module: IHP.Postgres.Point Description: Adds support for the Postgres Point type Copyright: (c) digitally induced GmbH, 2020 -} module IHP.Postgres.Point where import GHC.Float import BasicPrelude import Database.PostgreSQL.Simple.ToField import Database.PostgreSQL.Simple.FromField import qualified Database.PostgreSQL.Simple.TypeInfo.Static as TI import Database.PostgreSQL.Simple.TypeInfo.Macro as TI import Data.ByteString.Builder (byteString, char8) import Data.Attoparsec.ByteString.Char8 hiding (Result, char8, Parser(..)) import Data.Attoparsec.Internal.Types (Parser) import Data.Aeson -- | Represents a Postgres Point -- -- See https://www.postgresql.org/docs/9.5/datatype-geometric.html data Point = Point { x :: Double, y :: Double } deriving (Eq, Show, Ord) instance FromField Point where fromField f v = if typeOid f /= $(inlineTypoid TI.point) then returnError Incompatible f "" else case v of Nothing -> returnError UnexpectedNull f "" Just bs -> case parseOnly parsePoint bs of Left err -> returnError ConversionFailed f err Right val -> pure val parsePoint :: Parser ByteString Point parsePoint = do string "(" x <- doubleOrNaN string "," y <- doubleOrNaN string ")" pure $ Point { x, y } where -- Postgres supports storing NaN inside a point, so we have to deal -- with that here as well doubleOrNaN = double <|> (string "NaN" >> (pure $ 0 / 0)) instance ToField Point where toField = serializePoint serializePoint :: Point -> Action serializePoint Point { x, y } = Many [ Plain (byteString "point(") , toField x , Plain (char8 ',') , toField y , Plain (char8 ')') ] instance FromJSON Point where parseJSON = withObject "Point" $ \v -> Point <$> v .: "x" <*> v .: "y" instance ToJSON Point where toJSON Point { x, y } = object [ "x" .= x, "y" .= y ]