module Ganeti.Query.Filter
( compileFilter
, evaluateQueryFilter
, evaluateFilterM
, evaluateFilterJSON
, requestedNames
, makeSimpleFilter
, Comparator
, Comparison(..)
, toCompFun
, FilterOp(..)
) where
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 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 (fromJVal)
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 = forall a . (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 :: ErrorResult [String]
return $! val `elem` lst'
containsFilter (NumericValue val) lst = do
lst' <- fromJVal lst :: ErrorResult [Integer]
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