rhbzquery-0.4.3: src/ParseArg.hs
-- SPDX-License-Identifier: GPL-2.0-or-later
module ParseArg (
argHelp,
readBzQueryArg,
ArgType(..),
Operator(..),
operators,
showOp,
showOpHelp,
ProductVersion(..),
statusList
)
where
import Control.Applicative ((<|>))
import Data.Char
import Data.List.Extra as L
import Data.Version.Extra
import Numeric.Natural
import Common
argHelp :: String
argHelp = "[COMPONENT|STATUS|PRODUCTVERSION|FIELD=VALUE|FIELDopVALUE]..."
data Operator = Equals | NotEquals
| Substring | NotSubstring
| Regexp |NotRegexp
| CaseSubstring
| AnyWordsSubstr | AllWordsSubstr | NoWordsSubstr
| AnyExact
| AnyWords | AllWords | NoWords
| IsEmpty | IsNotEmpty
| NoOp
-- | ContentMatches | ContentNotMatch
deriving (Eq,Show,Enum,Bounded)
showOp :: Operator -> String
showOp = lower . show
operators :: [Operator]
operators = enumFromTo minBound maxBound
data OperatorData = OpUnary String String | OpNull String String
deriving Eq
opSyntax :: OperatorData -> String
opSyntax (OpUnary s _) = s
opSyntax (OpNull s _) = s
opDescribe :: OperatorData -> String
opDescribe (OpUnary _ h) = h
opDescribe (OpNull _ h) = h
showOpHelp :: Operator -> String
showOpHelp op =
let opd = opData op
in padSyntax opd ++ showOp op ++ " (" ++ opDescribe opd ++ ")"
where
padSyntax opd =
let ops = opSyntax opd
in replicate (10 - length ops) ' ' ++ "'" ++ ops ++ "' : "
instance Ord OperatorData where
compare op1 op2 =
let sop1 = opSyntax op1
sop2 = opSyntax op2
in
if sop1 `isInfixOf` sop2 then GT
else if sop2 `isInfixOf` sop1 then LT
else if sop1 == sop2
then error' $ sop1 ++ " listed twice in 'operators'"
else compare (reverse sop1) (reverse sop2)
-- FIXME check all unique
opData :: Operator -> OperatorData
opData NoOp = OpNull "~noop~" "ignore (comment out query field)"
opData Equals = OpUnary "=" "is equal to"
opData NotEquals = OpUnary "!=" "is not equal to"
opData AnyExact = OpUnary "~anyexact=" "is equal to any of the strings"
opData Substring = OpUnary "~" "contains the string"
opData NotSubstring = OpUnary "!~" "does not contain the string"
opData CaseSubstring = OpUnary "~case~" "contains the string (exact case)"
opData AnyWordsSubstr = OpUnary "~any~" "contains any of the strings"
opData AllWordsSubstr = OpUnary "~all~" "contains all of the strings"
opData NoWordsSubstr = OpUnary "~no~" "contains none of the strings"
opData Regexp = OpUnary "=~" "matches regular expression"
opData NotRegexp = OpUnary "!=~" "does not match regular expression"
opData AnyWords = OpUnary "~anywords~" "contains any of the words"
opData AllWords = OpUnary "~allwords~" "contains all of the words"
opData NoWords = OpUnary "~nowords~" "contains none of the words"
opData IsEmpty = OpNull "~empty~" "is empty"
opData IsNotEmpty = OpNull "~notempty~" "is not empty"
-- -- only for 'content':
-- opData Matches = OpUnary "~m~"
-- opData NotMatches = OpUnary "!m~"
data ArgType = ArgProdVer ProductVersion
| ArgStatusAll
| ArgStatusBefore String
| ArgStatusAfter String
| ArgParameter String Operator String
| ArgParameterEmpty String Bool
| ArgComponent String
readBzQueryArg :: String -> Maybe ArgType
readBzQueryArg s =
ArgProdVer <$> readProductVersion s <|>
parseStatus s <|>
parseField s <|>
parseComponent s
data ProductVersion = Fedora (Maybe Natural)
| Rawhide
| EPEL (Maybe Natural)
| RHEL Version
readProductVersion :: String -> Maybe ProductVersion
readProductVersion "fedora" = Just (Fedora Nothing)
readProductVersion "rawhide" = Just Rawhide
readProductVersion ('f':ver) | all isDigit ver = Just $ Fedora (Just (read ver :: Natural))
readProductVersion "epel" = Just (EPEL Nothing)
readProductVersion ('e':'p':'e':'l':v) | all isDigit v = Just (EPEL (Just (read v :: Natural)))
readProductVersion ('r':'h':'e':'l':ver) = Just $ RHEL (readVersion ver)
readProductVersion _ = Nothing
statusList :: [String]
statusList = ["NEW", "ASSIGNED", "POST", "MODIFIED", "ON_QA", "VERIFIED", "RELEASE_PENDING", "CLOSED"]
parseStatus :: String -> Maybe ArgType
parseStatus s =
case upper s of
"" -> error' "malformed status string"
"ALL" -> Just ArgStatusAll
('<':st) -> ArgStatusBefore <$> find (== st) statusList
('>':st) -> ArgStatusAfter <$> find (== st) statusList
caps -> ArgParameter "bug_status" Equals <$> find (== caps) statusList
parseField :: String -> Maybe ArgType
parseField ps =
foldr ((<|>) . parseParam) Nothing $ sortOn opData operators
where
parseParam :: Operator -> Maybe ArgType
parseParam oper =
case opData oper of
OpUnary op _ ->
if op `isInfixOf` ps then
case splitOn op ps of
(f:val) -> Just (ArgParameter f oper (intercalate op val))
_ -> Nothing
else Nothing
OpNull op _ ->
case stripSuffix op ps of
Just a ->
if null a
then error' $ "bad parameter: " ++ ps
else Just (ArgParameterEmpty a (oper == IsEmpty))
_ -> Nothing
-- https://fedoraproject.org/wiki/Packaging:Naming?rd=Packaging:NamingGuidelines#Common_Character_Set_for_Package_Naming
-- abcdefghijklmnopqrstuvwxyz
-- ABCDEFGHIJKLMNOPQRSTUVWXYZ
-- 0123456789-._+
parseComponent :: String -> Maybe ArgType
parseComponent ps =
if all isPackageChar ps
then Just (ArgComponent ps)
else error' $ "Invalid component name: " ++ ps
where
isPackageChar :: Char -> Bool
isPackageChar c =
-- Bugzilla components can contain a space though
isAsciiUpper c || isAsciiLower c || isDigit c || c `elem` " -._+"