{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Database.InfluxDB.Format
  ( -- $setup

  -- * The 'Format' type and associated functions
    Format
  , makeFormat
  , (%)

  -- * Formatting functions
  , formatQuery
  , formatDatabase
  , formatMeasurement
  , formatKey

  -- * Formatters for various types
  , database
  , key
  , keys
  , measurement
  , measurements
  , field
  , decimal
  , realFloat
  , text
  , string
  , byteString8
  , time

  -- * Utility functions
  , fromQuery
  ) where
import Control.Category
import Data.Monoid
import Data.String
import Prelude hiding ((.), id)

import Data.Time
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Builder as BB
import qualified Data.List as L
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TL
import qualified Data.Text.Lazy.Builder.Int as TL
import qualified Data.Text.Lazy.Builder.RealFloat as TL

import Database.InfluxDB.Internal.Text
import Database.InfluxDB.Types hiding (database)

{- $setup
This module is desined to be imported qualified:

>>> :set -XOverloadedStrings
>>> import qualified Data.ByteString as B
>>> import qualified Database.InfluxDB.Format as F
-}

-- | Serialize a 'Query' to a 'B.ByteString'.
fromQuery :: Query -> B.ByteString
fromQuery :: Query -> ByteString
fromQuery (Query Text
q) =
  ByteString -> ByteString
BL.toStrict forall a b. (a -> b) -> a -> b
$ Builder -> ByteString
BB.toLazyByteString forall a b. (a -> b) -> a -> b
$ Text -> Builder
T.encodeUtf8Builder Text
q

-- | A typed format string. @Format a r@ means that @a@ is the type of formatted
-- string, and @r@ is the type of the formatter.
--
-- >>> :t F.formatQuery
-- F.formatQuery :: F.Format Query r -> r
-- >>> :t F.key
-- F.key :: F.Format r (Key -> r)
-- >>> :t "SELECT * FROM "%F.key
-- "SELECT * FROM "%F.key :: F.Format a (Key -> a)
-- >>> :t F.formatQuery ("SELECT * FROM "%F.key)
-- F.formatQuery ("SELECT * FROM "%F.key) :: Key -> Query
-- >>> F.formatQuery ("SELECT * FROM "%F.key) "series"
-- "SELECT * FROM \"series\""
newtype Format a r = Format { forall a r. Format a r -> (Builder -> a) -> r
runFormat :: (TL.Builder -> a) -> r }

-- | 'Format's can be composed using @('.')@ from "Control.Category".
--
-- >>> import Control.Category ((.))
-- >>> import Prelude hiding ((.))
-- >>> F.formatQuery ("SELECT * FROM " . F.key) "series"
-- "SELECT * FROM \"series\""
instance Category Format where
  id :: forall a. Format a a
id = forall a r. ((Builder -> a) -> r) -> Format a r
Format (\Builder -> a
k -> Builder -> a
k Builder
"")
  Format b c
fmt1 . :: forall b c a. Format b c -> Format a b -> Format a c
. Format a b
fmt2 = forall a r. ((Builder -> a) -> r) -> Format a r
Format forall a b. (a -> b) -> a -> b
$ \Builder -> a
k ->
    forall a r. Format a r -> (Builder -> a) -> r
runFormat Format b c
fmt1 forall a b. (a -> b) -> a -> b
$ \Builder
a ->
      forall a r. Format a r -> (Builder -> a) -> r
runFormat Format a b
fmt2 forall a b. (a -> b) -> a -> b
$ \Builder
b ->
        Builder -> a
k (Builder
a forall a. Semigroup a => a -> a -> a
<> Builder
b)

-- | With the OverloadedStrings exension, string literals can be used to write
-- queries.
--
-- >>> "SELECT * FROM series" :: Query
-- "SELECT * FROM series"
instance a ~ r => IsString (Format a r) where
  fromString :: String -> Format a r
fromString String
xs = forall a r. ((Builder -> a) -> r) -> Format a r
Format forall a b. (a -> b) -> a -> b
$ \Builder -> a
k -> Builder -> a
k forall a b. (a -> b) -> a -> b
$ forall a. IsString a => String -> a
fromString String
xs

-- | 'Format' specific synonym of @('.')@.
--
-- This is typically easier to use than @('.')@ is because it doesn't
-- conflict with @Prelude.(.)@.
(%) :: Format b c -> Format a b -> Format a c
% :: forall b c a. Format b c -> Format a b -> Format a c
(%) = forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
(.)

runFormatWith :: (T.Text -> a) -> Format a r -> r
runFormatWith :: forall a r. (Text -> a) -> Format a r -> r
runFormatWith Text -> a
f Format a r
fmt = forall a r. Format a r -> (Builder -> a) -> r
runFormat Format a r
fmt (Text -> a
f forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> Text
TL.toStrict forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Builder -> Text
TL.toLazyText)

-- | Format a 'Query'.
--
-- >>> F.formatQuery "SELECT * FROM series"
-- "SELECT * FROM series"
-- >>> F.formatQuery ("SELECT * FROM "%F.key) "series"
-- "SELECT * FROM \"series\""
formatQuery :: Format Query r -> r
formatQuery :: forall r. Format Query r -> r
formatQuery = forall a r. (Text -> a) -> Format a r -> r
runFormatWith Text -> Query
Query

-- | Format a 'Database'.
--
-- >>> F.formatDatabase "test-db"
-- "test-db"
-- >>> F.formatDatabase ("test-db-"%F.decimal) 0
-- "test-db-0"
formatDatabase :: Format Database r -> r
formatDatabase :: forall r. Format Database r -> r
formatDatabase = forall a r. (Text -> a) -> Format a r -> r
runFormatWith Text -> Database
Database

-- | Format a 'Measurement'.
--
-- >>> F.formatMeasurement "test-series"
-- "test-series"
-- >>> F.formatMeasurement ("test-series-"%F.decimal) 0
-- "test-series-0"
formatMeasurement :: Format Measurement r -> r
formatMeasurement :: forall r. Format Measurement r -> r
formatMeasurement = forall a r. (Text -> a) -> Format a r -> r
runFormatWith Text -> Measurement
Measurement

-- | Format a 'Key'.
--
-- >>> F.formatKey "test-key"
-- "test-key"
-- >>> F.formatKey ("test-key-"%F.decimal) 0
-- "test-key-0"
formatKey :: Format Key r -> r
formatKey :: forall r. Format Key r -> r
formatKey Format Key r
fmt = forall a r. Format a r -> (Builder -> a) -> r
runFormat Format Key r
fmt (Text -> Key
Key forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> Text
TL.toStrict forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Builder -> Text
TL.toLazyText)

-- | Convenience function to make a custom formatter.
makeFormat :: (a -> TL.Builder) -> Format r (a -> r)
makeFormat :: forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat a -> Builder
build = forall a r. ((Builder -> a) -> r) -> Format a r
Format forall a b. (a -> b) -> a -> b
$ \Builder -> r
k a
a -> Builder -> r
k forall a b. (a -> b) -> a -> b
$ a -> Builder
build a
a

doubleQuote :: T.Text -> TL.Builder
doubleQuote :: Text -> Builder
doubleQuote Text
name = Builder
"\"" forall a. Semigroup a => a -> a -> a
<> Text -> Builder
TL.fromText Text
name forall a. Semigroup a => a -> a -> a
<> Builder
"\""

singleQuote :: T.Text -> TL.Builder
singleQuote :: Text -> Builder
singleQuote Text
name = Builder
"'" forall a. Semigroup a => a -> a -> a
<> Text -> Builder
TL.fromText Text
name forall a. Semigroup a => a -> a -> a
<> Builder
"'"

identifierBuilder :: T.Text -> TL.Builder
identifierBuilder :: Text -> Builder
identifierBuilder = Text -> Builder
doubleQuote forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> Text
escapeDoubleQuotes

stringBuilder :: T.Text -> TL.Builder
stringBuilder :: Text -> Builder
stringBuilder = Text -> Builder
singleQuote forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> Text
escapeSingleQuotes

-- | Format a database name.
--
-- >>> F.formatQuery ("CREATE DATABASE "%F.database) "test-db"
-- "CREATE DATABASE \"test-db\""
database :: Format r (Database -> r)
database :: forall r. Format r (Database -> r)
database = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a b. (a -> b) -> a -> b
$ \(Database Text
name) -> Text -> Builder
identifierBuilder Text
name

-- | Format an identifier (e.g. field names, tag names, etc).
--
-- Identifiers in InfluxDB protocol are surrounded with double quotes.
--
-- >>> F.formatQuery ("SELECT "%F.key%" FROM series") "field"
-- "SELECT \"field\" FROM series"
-- >>> F.formatQuery ("SELECT "%F.key%" FROM series") "foo\"bar"
-- "SELECT \"foo\\\"bar\" FROM series"
key :: Format r (Key -> r)
key :: forall r. Format r (Key -> r)
key = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a b. (a -> b) -> a -> b
$ \(Key Text
name) -> Text -> Builder
identifierBuilder Text
name

-- | Format multiple keys.
--
-- >>> F.formatQuery ("SELECT "%F.keys%" FROM series") ["field1", "field2"]
-- "SELECT \"field1\",\"field2\" FROM series"
keys :: Format r ([Key] -> r)
keys :: forall r. Format r ([Key] -> r)
keys = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a b. (a -> b) -> a -> b
$
  forall a. Monoid a => [a] -> a
mconcat forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. forall a. a -> [a] -> [a]
L.intersperse Builder
"," forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. forall a b. (a -> b) -> [a] -> [b]
map (\(Key Text
name) -> Text -> Builder
identifierBuilder Text
name)

-- | Format a measurement.
--
-- >>> F.formatQuery ("SELECT * FROM "%F.measurement) "test-series"
-- "SELECT * FROM \"test-series\""
measurement :: Format r (Measurement -> r)
measurement :: forall r. Format r (Measurement -> r)
measurement = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a b. (a -> b) -> a -> b
$ \(Measurement Text
name) -> Text -> Builder
identifierBuilder Text
name

-- | Format a measurement.
--
-- >>> F.formatQuery ("SELECT * FROM "%F.measurements) ["series1", "series2"]
-- "SELECT * FROM \"series1\",\"series2\""
measurements :: Format r ([Measurement] -> r)
measurements :: forall r. Format r ([Measurement] -> r)
measurements = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a b. (a -> b) -> a -> b
$
  forall a. Monoid a => [a] -> a
mconcat forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. forall a. a -> [a] -> [a]
L.intersperse Builder
","
    forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. forall a b. (a -> b) -> [a] -> [b]
map (\(Measurement Text
name) -> Text -> Builder
identifierBuilder Text
name)

-- | Format an InfluxDB value. Good for field and tag values.
--
-- >>> F.formatQuery ("SELECT * FROM series WHERE "%F.key%" = "%F.field) "location" "tokyo"
-- "SELECT * FROM series WHERE \"location\" = 'tokyo'"
field :: Format r (QueryField -> r)
field :: forall r. Format r (QueryField -> r)
field = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a b. (a -> b) -> a -> b
$ \case
  FieldInt Int64
n -> forall a. Integral a => a -> Builder
TL.decimal Int64
n
  FieldFloat Double
d -> forall a. RealFloat a => a -> Builder
TL.realFloat Double
d
  FieldString Text
s -> Text -> Builder
stringBuilder Text
s
  FieldBool Bool
b -> if Bool
b then Builder
"true" else Builder
"false"
  QueryField
FieldNull -> Builder
"null"

-- | Format a decimal number.
--
-- >>> F.formatQuery ("SELECT * FROM series WHERE time < now() - "%F.decimal%"h") 1
-- "SELECT * FROM series WHERE time < now() - 1h"
decimal :: Integral a => Format r (a -> r)
decimal :: forall a r. Integral a => Format r (a -> r)
decimal = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a. Integral a => a -> Builder
TL.decimal

-- | Format a floating-point number.
--
-- >>> F.formatQuery ("SELECT * FROM series WHERE value > "%F.realFloat) 0.1
-- "SELECT * FROM series WHERE value > 0.1"
realFloat :: RealFloat a => Format r (a -> r)
realFloat :: forall a r. RealFloat a => Format r (a -> r)
realFloat = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a. RealFloat a => a -> Builder
TL.realFloat

-- | Format a text.
--
-- Note that this doesn't escape the string. Use 'formatKey' to format field
-- values in a query.
--
-- >>> :t F.formatKey F.text
-- F.formatKey F.text :: T.Text -> Key
text :: Format r (T.Text -> r)
text :: forall r. Format r (Text -> r)
text = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat Text -> Builder
TL.fromText

-- | Format a string.
--
-- Note that this doesn't escape the string. Use 'formatKey' to format field
-- values in a query.
--
-- >>> :t F.formatKey F.string
-- F.formatKey F.string :: String -> Key
string :: Format r (String -> r)
string :: forall r. Format r (String -> r)
string = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat String -> Builder
TL.fromString

-- | Format a UTF-8 encoded byte string.
--
-- Note that this doesn't escape the string. Use 'formatKey' to format field
-- values in a query.
--
-- >>> :t F.formatKey F.byteString8
-- F.formatKey F.byteString8 :: B.ByteString -> Key
byteString8 :: Format r (B.ByteString -> r)
byteString8 :: forall r. Format r (ByteString -> r)
byteString8 = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a b. (a -> b) -> a -> b
$ Text -> Builder
TL.fromText forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. ByteString -> Text
T.decodeUtf8

-- | Format a time.
--
-- >>> import Data.Time
-- >>> let Just t = parseTimeM False defaultTimeLocale "%s" "0" :: Maybe UTCTime
-- >>> F.formatQuery ("SELECT * FROM series WHERE time >= "%F.time) t
-- "SELECT * FROM series WHERE time >= '1970-01-01 00:00:00'"
time :: FormatTime time => Format r (time -> r)
time :: forall time r. FormatTime time => Format r (time -> r)
time = forall a r. (a -> Builder) -> Format r (a -> r)
makeFormat forall a b. (a -> b) -> a -> b
$ \time
t ->
  Builder
"'" forall a. Semigroup a => a -> a -> a
<> String -> Builder
TL.fromString (forall t. FormatTime t => TimeLocale -> String -> t -> String
formatTime TimeLocale
defaultTimeLocale String
fmt time
t) forall a. Semigroup a => a -> a -> a
<> Builder
"'"
  where
    fmt :: String
fmt = String
"%F %X%Q" -- YYYY-MM-DD HH:MM:SS.nnnnnnnnn