module Ganeti.Query.Filter
( compileFilter
, evaluateQueryFilter
, evaluateFilterM
, evaluateFilterJSON
, requestedNames
, makeSimpleFilter
, Comparator
, Comparison(..)
, toCompFun
, FilterOp(..)
) where
import Control.Applicative
import Control.Monad (liftM, mzero)
import Control.Monad.Trans.Maybe (MaybeT, runMaybeT)
import Control.Monad.Trans.Class (lift)
import qualified Data.Map as Map
import Data.Maybe
import Data.Traversable (traverse)
import Text.JSON (JSValue(..), fromJSString)
import Text.JSON.Pretty (pp_value)
import qualified Text.Regex.PCRE as PCRE
import Ganeti.BasicTypes
import Ganeti.Errors
import Ganeti.Objects
import Ganeti.Query.Language
import Ganeti.Query.Types
import Ganeti.Utils.Monad (anyM, allM)
import Ganeti.JSON
compileFilter :: FieldMap a b
-> Filter FilterField
-> ErrorResult (Filter (FieldGetter a b, QffMode))
compileFilter fm =
traverse (\field -> maybe
(Bad . ParameterError $ "Can't find field named '" ++
field ++ "'")
(\(_, g, q) -> Ok (g, q)) (field `Map.lookup` fm))
qffField :: QffMode -> JSValue -> ErrorResult JSValue
qffField QffNormal v = Ok v
qffField QffHostname v = Ok v
qffField QffTimestamp v =
case v of
JSArray [secs@(JSRational _ _), JSRational _ _] -> return secs
_ -> Bad $ ProgrammerError
"Internal error: Getter returned non-timestamp for QffTimestamp"
wrapGetter :: ConfigData
-> Maybe b
-> a
-> (FieldGetter a b, QffMode)
-> (JSValue -> ErrorResult Bool)
-> MaybeT ErrorResult Bool
wrapGetter cfg b a (getter, qff) faction =
case tryGetter cfg b a getter of
Nothing -> mzero
Just v -> lift $
case v of
ResultEntry RSNormal (Just fval) -> qffField qff fval >>= faction
ResultEntry RSNormal Nothing ->
Bad $ ProgrammerError
"Internal error: Getter returned RSNormal/Nothing"
_ -> Ok True
trueFilter :: JSValue -> ErrorResult Bool
trueFilter (JSBool x) = Ok $! x
trueFilter v = Bad . ParameterError $
"Unexpected value '" ++ show (pp_value v) ++
"' in boolean context"
type Comparator = (Eq a, Ord a) => a -> a -> Bool
eqFilter :: QffMode -> FilterValue -> JSValue -> ErrorResult Bool
eqFilter QffNormal flv jsv = binOpFilter (==) flv jsv
eqFilter QffTimestamp flv jsv = binOpFilter (==) flv jsv
eqFilter QffHostname _ (JSRational _ _) =
Bad . ProgrammerError $ "QffHostname field returned a numeric value"
eqFilter QffHostname (QuotedString y) (JSString x) =
Ok $ goodLookupResult (fromJSString x `compareNameComponent` y)
eqFilter _ flv jsv = binOpFilter (==) flv jsv
binOpFilter :: Comparator -> FilterValue -> JSValue -> ErrorResult Bool
binOpFilter comp (QuotedString y) (JSString x) =
Ok $! fromJSString x `comp` y
binOpFilter comp (NumericValue y) (JSRational _ x) =
Ok $! x `comp` fromIntegral y
binOpFilter _ expr actual =
Bad . ParameterError $ "Invalid types in comparison, trying to compare " ++
show (pp_value actual) ++ " with '" ++ show expr ++ "'"
regexpFilter :: FilterRegex -> JSValue -> ErrorResult Bool
regexpFilter re (JSString val) =
Ok $! PCRE.match (compiledRegex re) (fromJSString val)
regexpFilter _ x =
Bad . ParameterError $ "Invalid field value used in regexp matching,\
\ expecting string but got '" ++ show (pp_value x) ++ "'"
containsFilter :: FilterValue -> JSValue -> ErrorResult Bool
containsFilter (QuotedString val) lst = do
lst' <- fromJVal lst
return $! val `elem` lst'
containsFilter (NumericValue val) lst = do
lst' <- fromJVal lst
return $! val `elem` lst'
data Comparison = Eq | Lt | Le | Gt | Ge
deriving (Eq, Ord, Show)
toCompFun :: Comparison -> Comparator
toCompFun cmp = case cmp of
Eq -> (==)
Lt -> (<)
Le -> (<=)
Gt -> (>)
Ge -> (>=)
data FilterOp field val where
Truth :: FilterOp field ()
Comp :: Comparison -> FilterOp field FilterValue
Regex :: FilterOp field FilterRegex
Contains :: FilterOp field FilterValue
deriving instance Eq (FilterOp field val)
deriving instance Show (FilterOp field val)
evaluateFilterM :: (Monad m, Applicative m)
=> (forall val .
FilterOp field val -> field -> val -> m Bool)
-> Filter field
-> m Bool
evaluateFilterM opFun fil = case fil of
EmptyFilter -> return True
AndFilter flts -> allM recurse flts
OrFilter flts -> anyM recurse flts
NotFilter flt -> not <$> recurse flt
TrueFilter field -> opFun Truth field ()
EQFilter field val -> opFun (Comp Eq) field val
LTFilter field val -> opFun (Comp Lt) field val
LEFilter field val -> opFun (Comp Le) field val
GTFilter field val -> opFun (Comp Gt) field val
GEFilter field val -> opFun (Comp Ge) field val
RegexpFilter field re -> opFun Regex field re
ContainsFilter field val -> opFun Contains field val
where
recurse = evaluateFilterM opFun
evaluateQueryFilter :: ConfigData -> Maybe b -> a
-> Filter (FieldGetter a b, QffMode)
-> ErrorResult Bool
evaluateQueryFilter c mb a =
fmap (fromMaybe True) . runMaybeT . evaluateFilterM (\op -> case op of
Truth -> \gQff () -> wrap gQff trueFilter
Comp Eq -> \gQff val -> wrap gQff $ eqFilter (snd gQff) val
Comp cmp -> \gQff val -> wrap gQff $ binOpFilter (toCompFun cmp) val
Regex -> \gQff re -> wrap gQff $ regexpFilter re
Contains -> \gQff val -> wrap gQff $ containsFilter val
)
where
wrap = wrapGetter c mb a
evaluateFilterJSON :: Filter JSValue -> ErrorResult Bool
evaluateFilterJSON =
evaluateFilterM $ \op -> case op of
Comp cmp -> let compFun = toCompFun cmp
in \json fv -> pure $ json `compFun` showFilterValue fv
Truth -> \field () -> trueFilter field
Regex -> flip regexpFilter
Contains -> flip containsFilter
tryGetter :: ConfigData -> Maybe b -> a -> FieldGetter a b -> Maybe ResultEntry
tryGetter _ _ item (FieldSimple getter) = Just $ getter item
tryGetter cfg _ item (FieldConfig getter) = Just $ getter cfg item
tryGetter _ rt item (FieldRuntime getter) =
maybe Nothing (\rt' -> Just $ getter rt' item) rt
tryGetter cfg rt item (FieldConfigRuntime getter) =
maybe Nothing (\rt' -> Just $ getter cfg rt' item) rt
tryGetter _ _ _ FieldUnknown = Just $ ResultEntry RSUnknown Nothing
requestedNames :: FilterField -> Filter FilterField -> Maybe [FilterValue]
requestedNames _ EmptyFilter = Just []
requestedNames namefield (OrFilter flts) =
liftM concat $ mapM (requestedNames namefield) flts
requestedNames namefield (EQFilter fld val) =
if namefield == fld
then Just [val]
else Nothing
requestedNames _ _ = Nothing
makeSimpleFilter :: String -> [Either String Integer] -> Filter FilterField
makeSimpleFilter _ [] = EmptyFilter
makeSimpleFilter namefield vals =
OrFilter $ map (EQFilter namefield . either QuotedString NumericValue) vals