claims-x12-dsl-0.1.0.0: src/Claims/Parser.hs
{-# LANGUAGE OverloadedStrings #-}
module Claims.Parser
( parseRule
, parseRules
, convertToInternalRule
) where
import Claims.Parser.AST
import qualified Claims.Types as T
import Claims.Types (Rule, ClaimType(..), ValidationResult)
import Text.Parsec
import Text.Parsec.String (Parser)
import Text.Parsec.Expr (buildExpressionParser, Operator(..), Assoc(..))
import qualified Text.Parsec.Token as P
import Text.Parsec.Language (emptyDef)
import Data.Text (pack)
import Data.Decimal (realFracToDecimal)
-- Lexer definition
lexer :: P.TokenParser ()
lexer = P.makeTokenParser emptyDef
{ P.reservedNames =
[ "RULE", "DESCRIPTION", "WHEN", "THEN", "ELSE", "END"
, "AND", "OR", "NOT", "BETWEEN"
, "APPROVE", "REJECT", "REQUIRE_REVIEW"
, "claim", "amount", "type", "place_of_service"
, "has_diagnosis", "has_procedure"
, "Inpatient", "Outpatient", "Professional"
]
, P.reservedOpNames = [">", "<", "=", ">=", "<="]
}
reserved :: String -> Parser ()
reserved = P.reserved lexer
reservedOp :: String -> Parser ()
reservedOp = P.reservedOp lexer
identifier :: Parser String
identifier = P.identifier lexer
stringLiteral :: Parser String
stringLiteral = P.stringLiteral lexer
float :: Parser Double
float = P.float lexer
natural :: Parser Integer
natural = P.natural lexer
whiteSpace :: Parser ()
whiteSpace = P.whiteSpace lexer
dot :: Parser String
dot = P.dot lexer
-- | Parse a complete rule
parseRule :: Parser ParsedRule
parseRule = do
whiteSpace
reserved "RULE"
name <- identifier
reserved "DESCRIPTION"
desc <- stringLiteral
reserved "WHEN"
cond <- parseCondition
reserved "THEN"
act <- parseAction
elseAct <- optionMaybe parseElseWhen
reserved "END"
return $ ParsedRule name desc cond act elseAct
-- | Parse ELSE WHEN clause
parseElseWhen :: Parser (Condition, Action)
parseElseWhen = do
reserved "ELSE"
reserved "WHEN"
cond <- parseCondition
reserved "THEN"
act <- parseAction
return (cond, act)
-- | Parse a condition expression
parseCondition :: Parser Condition
parseCondition = buildExpressionParser table term
where
table =
[ [Prefix (reserved "NOT" >> return NotCond)]
, [Infix (reserved "AND" >> return AndCond) AssocLeft]
, [Infix (reserved "OR" >> return OrCond) AssocLeft]
]
term = parens parseCondition
<|> try parseAmountBetween
<|> try parseAmountComparison
<|> try parseHasDiagnosis
<|> try parseHasProcedure
<|> try parsePlaceOfService
<|> try parseClaimType
parens :: Parser a -> Parser a
parens = P.parens lexer
-- | Parse amount comparison (e.g., claim.amount > 50000)
parseAmountComparison :: Parser Condition
parseAmountComparison = do
reserved "claim"
_ <- dot
reserved "amount"
op <- parseCompOp
value <- try float <|> fmap fromInteger natural
return $ CompareAmount op value
-- | Parse amount between (e.g., claim.amount BETWEEN 0 AND 100)
parseAmountBetween :: Parser Condition
parseAmountBetween = do
reserved "claim"
_ <- dot
reserved "amount"
reserved "BETWEEN"
low <- try float <|> fmap fromInteger natural
reserved "AND"
high <- try float <|> fmap fromInteger natural
return $ AmountBetween low high
-- | Parse has diagnosis code (e.g., claim.has_diagnosis "S06")
parseHasDiagnosis :: Parser Condition
parseHasDiagnosis = do
reserved "claim"
_ <- dot
reserved "has_diagnosis"
code <- stringLiteral
return $ CheckDiagnosis code
-- | Parse has procedure code (e.g., claim.has_procedure "99221")
parseHasProcedure :: Parser Condition
parseHasProcedure = do
reserved "claim"
_ <- dot
reserved "has_procedure"
code <- stringLiteral
return $ CheckProcedure code
-- | Parse place of service (e.g., claim.place_of_service = "23")
parsePlaceOfService :: Parser Condition
parsePlaceOfService = do
reserved "claim"
_ <- dot
reserved "place_of_service"
reservedOp "="
pos <- stringLiteral
return $ CheckPlaceOfService pos
-- | Parse claim type (e.g., claim.type = "Inpatient")
parseClaimType :: Parser Condition
parseClaimType = do
reserved "claim"
_ <- dot
reserved "type"
reservedOp "="
ct <- stringLiteral
return $ CheckClaimType ct
-- | Parse comparison operator
parseCompOp :: Parser CompOp
parseCompOp =
(reservedOp ">=" >> return Gte)
<|> (reservedOp "<=" >> return Lte)
<|> (reservedOp ">" >> return Gt)
<|> (reservedOp "<" >> return Lt)
<|> (reservedOp "=" >> return Eq)
-- | Parse an action
parseAction :: Parser Action
parseAction =
(reserved "APPROVE" >> return ApproveClaim)
<|> (reserved "REJECT" >> RejectClaim <$> stringLiteral)
<|> (reserved "REQUIRE_REVIEW" >> RequireReview <$> stringLiteral)
-- | Parse multiple rules from a file
parseRules :: String -> Either ParseError [ParsedRule]
parseRules input = parse (many parseRule <* eof) "" input
-- | Convert a parsed external DSL rule to an internal DSL rule
convertToInternalRule :: ParsedRule -> Rule ValidationResult
convertToInternalRule pr =
case elseAction pr of
Nothing -> T.If (convertCondition (conditions pr)) (convertAction (action pr)) T.approve
Just (elseCond, elseAct) ->
T.If (convertCondition (conditions pr))
(convertAction (action pr))
(T.If (convertCondition elseCond) (convertAction elseAct) T.approve)
-- | Convert a parsed condition to an internal rule condition
convertCondition :: Condition -> Rule Bool
convertCondition cond = case cond of
CompareAmount op value ->
let amt = realFracToDecimal 2 value
epsilon = realFracToDecimal 2 (0.01 :: Double)
in case op of
Gt -> T.greaterThan amt
Lt -> T.lessThan amt
Eq -> T.AmountGreaterThan amt `T.And` T.AmountLessThan (amt + epsilon)
Gte -> T.greaterThan amt `T.Or` (T.AmountGreaterThan amt `T.And` T.AmountLessThan (amt + epsilon))
Lte -> T.lessThan amt `T.Or` (T.AmountGreaterThan amt `T.And` T.AmountLessThan (amt + epsilon))
AmountBetween low high -> T.between (realFracToDecimal 2 low) (realFracToDecimal 2 high)
CheckDiagnosis code -> T.hasDx (pack code)
CheckProcedure code -> T.hasPx (pack code)
CheckPlaceOfService p -> T.pos (pack p)
CheckClaimType ct -> T.isType (convertClaimType ct)
AndCond c1 c2 -> convertCondition c1 `T.And` convertCondition c2
OrCond c1 c2 -> convertCondition c1 `T.Or` convertCondition c2
NotCond c -> T.Not (convertCondition c)
-- | Convert a parsed action to an internal rule action
convertAction :: Action -> Rule ValidationResult
convertAction act = case act of
ApproveClaim -> T.approve
RejectClaim msg -> T.reject (pack msg)
RequireReview msg -> T.needsReview (pack msg)
-- | Convert a claim type string to ClaimType
convertClaimType :: String -> ClaimType
convertClaimType "Inpatient" = Inpatient
convertClaimType "Outpatient" = Outpatient
convertClaimType "Professional" = Professional
convertClaimType _ = Professional -- default