{-# LANGUAGE TupleSections #-} {-| Parse puppet source code from text. -} module Puppet.Parser ( -- * Runner runPuppetParser -- * Parsers , Parser , puppetParser , PuppetParseError , prettyParseError -- ** exposed to ease testing , expression , datatype -- * Pretty Print , module Puppet.Parser.PrettyPrinter , module Puppet.Parser.Types , module Puppet.Parser.Lens ) where import XPrelude.Extra hiding (option, try, many, some) import XPrelude.PP hiding (braces, comma, brackets, parens, sep) import qualified Data.Char as Char import qualified Data.List as List import qualified Data.List.NonEmpty as NE import qualified Data.Maybe.Strict as S import qualified Data.Scientific as Scientific import qualified Data.Text as Text import qualified Data.Vector as V import Text.Megaparsec import Text.Megaparsec.Char import qualified Text.Megaparsec.Char.Lexer as Lexer import Text.Megaparsec.Expr import qualified Text.Regex.PCRE.ByteString.Utils as Regex import Puppet.Language import Puppet.Parser.Lens import Puppet.Parser.PrettyPrinter import Puppet.Parser.Types type PuppetParseError = ParseError Char Void type Parser = Parsec Void Text -- | Build a 'PrettyError' from a 'ParseError' given the text source. -- The source is used to display the line on which the error occurs. prettyParseError :: Text -> ParseError Char Void -> PrettyError prettyParseError s err = PrettyError $ "cannot parse" <+> pretty (parseErrorPretty' s err) -- | Run a puppet parser against some 'Text' input. runPuppetParser :: String -> Text -> Either PuppetParseError (Vector Statement) runPuppetParser src input = parse puppetParser src input -- space consumer sc :: Parser () sc = Lexer.space space1 (Lexer.skipLineComment "#") (Lexer.skipBlockComment "/*" "*/") lexeme :: Parser a -> Parser a lexeme = Lexer.lexeme sc symbol :: Text -> Parser () symbol = void . Lexer.symbol sc symbolic :: Char -> Parser () symbolic = symbol . Text.singleton integerOrDouble :: Parser (Either Integer Double) integerOrDouble = fmap Left hex <|> (either Right Left . Scientific.floatingOrInteger <$> Lexer.scientific) where hex = string "0x" *> Lexer.hexadecimal braces :: Parser a -> Parser a braces = between (symbol "{") (symbol "}") parens :: Parser a -> Parser a parens = between (symbol "(") (symbol ")") brackets :: Parser a -> Parser a brackets = between (symbol "[") (symbol "]") comma :: Parser () comma = symbol "," sepComma :: Parser a -> Parser [a] sepComma p = p `sepEndBy` comma sepComma1 :: Parser a -> Parser [a] sepComma1 p = p `sepEndBy1` comma -- | Parse a collection of puppet 'Statement'. puppetParser :: Parser (Vector Statement) puppetParser = optional sc >> statementList -- | Parse an 'Expression'. expression :: Parser Expression expression = condExpression <|> makeExprParser (lexeme terminal) expressionTable "expression" where condExpression = do selectedExpression <- try $ do trm <- lexeme terminal lookups <- optional indexLookupChain symbolic '?' return $ maybe trm ($ trm) lookups let cas = do c <- (SelectorDefault <$ symbol "default") -- default case <|> fmap SelectorType (try datatype) <|> fmap SelectorValue ( fmap UVariableReference variableReference <|> fmap UBoolean puppetBool <|> (UUndef <$ symbol "undef") <|> literalValue <|> fmap UInterpolable interpolableString <|> (URegexp <$> termRegexp) ) void $ symbol "=>" e <- expression return (c :!: e) cases <- braces (sepComma1 cas) return (ConditionalValue selectedExpression (V.fromList cases)) variable :: Parser Expression variable = Terminal . UVariableReference <$> variableReference stringLiteral' :: Parser Text stringLiteral' = char '\'' *> interior <* symbolic '\'' where interior = Text.pack . concat <$> many (some (noneOf ['\'', '\\']) <|> (char '\\' *> fmap escape anyChar)) escape '\'' = "'" escape x = ['\\',x] identifier :: Parser String identifier = some (satisfy identifierPart) identifierPart :: Char -> Bool identifierPart x = Char.isAsciiLower x || Char.isAsciiUpper x || Char.isDigit x || (x == '_') identl :: Parser Char -> Parser Char -> Parser Text identl fstl nxtl = do f <- fstl nxt <- lexeme $ many nxtl return $ Text.pack $ f : nxt operator :: Text -> Parser () operator = void . try . symbol reserved :: Text -> Parser () reserved s = try $ do void (string s) notFollowedBy (satisfy identifierPart) sc variableName :: Parser Text variableName = do let acceptablePart = Text.pack <$> many (satisfy identifierAcceptable) identifierAcceptable x = Char.isAsciiLower x || Char.isAsciiUpper x || Char.isDigit x || (x == '_') out <- qualif acceptablePart when (out == "string") (panic "The special variable $string should never be used") return out qualif :: Parser Text -> Parser Text qualif p = lexeme $ do header <- option "" (string "::") ( header <> ) . Text.intercalate "::" <$> p `sepBy1` string "::" qualif1 :: Parser Text -> Parser Text qualif1 p = try $ do r <- qualif p unless ("::" `Text.isInfixOf` r) (fail "This parser is not qualified") return r className :: Parser Text className = qualif moduleName -- yay with reserved words typeName :: Parser Text typeName = className moduleName :: Parser Text moduleName = genericModuleName False resourceNameRef :: Parser Text resourceNameRef = qualif (genericModuleName True) genericModuleName :: Bool -> Parser Text genericModuleName isReference = do let acceptable x = Char.isAsciiLower x || Char.isDigit x || (x == '_') firstletter = if isReference then fmap Char.toLower (satisfy Char.isAsciiUpper) else satisfy Char.isAsciiLower identl firstletter (satisfy acceptable) parameterName :: Parser Text parameterName = moduleName variableReference :: Parser Text variableReference = char '$' *> variableName interpolableString :: Parser (Vector Expression) interpolableString = V.fromList <$> between (char '"') (symbolic '"') ( many (interpolableVariableReference <|> doubleQuotedStringContent <|> fmap (Terminal . UString . Text.singleton) (char '$')) ) where doubleQuotedStringContent = Terminal . UString . Text.pack . concat <$> some ((char '\\' *> fmap escaper anyChar) <|> some (noneOf [ '"', '\\', '$' ])) escaper :: Char -> String escaper 'n' = "\n" escaper 't' = "\t" escaper 'r' = "\r" escaper '"' = "\"" escaper '\\' = "\\" escaper '$' = "$" escaper x = ['\\',x] -- this is specialized because we can't be "tokenized" here variableAccept x = Char.isAsciiLower x || Char.isAsciiUpper x || Char.isDigit x || x == '_' rvariableName = do v <- Text.concat <$> some (string "::" <|> fmap Text.pack (some (satisfy variableAccept))) when (v == "string") (fail "The special variable $string must not be used") return v rvariable = Terminal . UVariableReference <$> rvariableName simpleIndexing = Lookup <$> rvariable <*> between (symbolic '[') (symbolic ']') expression interpolableVariableReference = do void (char '$') let fenced = try (simpleIndexing <* char '}') <|> try (rvariable <* char '}') <|> (expression <* char '}') (symbolic '{' *> fenced) <|> try rvariable <|> pure (Terminal (UString (Text.singleton '$'))) regexp :: Parser Text regexp = do void (char '/') Text.pack . concat <$> many ( do { void (char '\\') ; x <- anyChar; return ['\\', x] } <|> some (noneOf [ '/', '\\' ]) ) <* symbolic '/' puppetArray :: Parser UnresolvedValue puppetArray = fmap (UArray . V.fromList) (brackets (sepComma expression)) "Array" puppetHash :: Parser UnresolvedValue puppetHash = fmap (UHash . V.fromList) (braces (sepComma hashPart)) "Hash" where hashPart = (:!:) <$> (expression <* operator "=>") <*> expression puppetBool :: Parser Bool puppetBool = (reserved "true" >> return True) <|> (reserved "false" >> return False) "Boolean" resourceReferenceRaw :: Parser (Text, [Expression]) resourceReferenceRaw = do restype <- resourceNameRef "Resource reference type" resnames <- brackets (expression `sepBy1` comma) "Resource reference values" return (restype, resnames) resourceReference :: Parser UnresolvedValue resourceReference = do (restype, resnames) <- resourceReferenceRaw return $ UResourceReference restype $ case resnames of [x] -> x _ -> Terminal $ UArray (V.fromList resnames) bareword :: Parser Text bareword = identl (satisfy Char.isAsciiLower) (satisfy acceptable) "Bare word" where acceptable x = Char.isAsciiLower x || Char.isAsciiUpper x || Char.isDigit x || (x == '_') || (x == '-') -- The first argument defines if non-parenthesized arguments are acceptable genFunctionCall :: Bool -> Parser (Text, Vector Expression) genFunctionCall nonparens = do fname <- moduleName "Function name" -- this is a hack. Contrary to what the documentation says, -- a "bareword" can perfectly be a qualified name : -- include foo::bar let argsc sep e = (fmap (Terminal . UString) (qualif1 className) <|> e "Function argument A") `sep` comma terminalF = terminalG (fail "function hack") expressionF = makeExprParser (lexeme terminalF) expressionTable "function expression" withparens = parens (argsc sepEndBy expression) withoutparens = argsc sepEndBy1 expressionF args <- withparens <|> if nonparens then withoutparens "Function arguments B" else fail "Function arguments C" return (fname, V.fromList args) literalValue :: Parser UnresolvedValue literalValue = lexeme (fmap UString stringLiteral' <|> fmap UString bareword <|> fmap UNumber numericalvalue "Literal Value") where numericalvalue = integerOrDouble >>= \i -> case i of Left x -> return (fromIntegral x) Right y -> return (Scientific.fromFloatDigits y) -- this is a hack for functions :( terminalG :: Parser Expression -> Parser Expression terminalG g = parens expression <|> fmap (Terminal . UInterpolable) interpolableString <|> (reserved "undef" *> return (Terminal UUndef)) <|> fmap (Terminal . URegexp) termRegexp <|> variable <|> fmap Terminal puppetArray <|> fmap Terminal puppetHash <|> fmap (Terminal . UBoolean) puppetBool <|> fmap (Terminal . UDataType) datatype <|> fmap Terminal resourceReference <|> g <|> fmap Terminal literalValue compileRegexp :: Text -> Parser CompRegex compileRegexp p = case Regex.compile' Regex.compBlank Regex.execBlank (encodeUtf8 p) of Right r -> return $ CompRegex p r Left ms -> fail ("Can't parse regexp /" ++ Text.unpack p ++ "/ : " ++ show ms) termRegexp :: Parser CompRegex termRegexp = regexp >>= compileRegexp terminal :: Parser Expression terminal = terminalG (fmap Terminal (fmap UHOLambdaCall (try lambdaCall) <|> try funcCall)) where funcCall :: Parser UnresolvedValue funcCall = do (fname, args) <- genFunctionCall False return $ UFunctionCall fname args expressionTable :: [[Operator Parser Expression]] expressionTable = [ [ Postfix indexLookupChain ] -- http://stackoverflow.com/questions/10475337/parsec-expr-repeated-prefix-postfix-operator-not-supported , [ Prefix ( operator "-" >> return Negate ) ] , [ Prefix ( operator "!" >> return Not ) ] , [ InfixL ( operator "." >> return FunctionApplication ) ] , [ InfixL ( reserved "in" >> return Contains ) ] , [ InfixL ( operator "/" >> return Division ) , InfixL ( operator "*" >> return Multiplication ) ] , [ InfixL ( operator "+" >> return Addition ) , InfixL ( operator "-" >> return Substraction ) ] , [ InfixL ( operator "<<" >> return LeftShift ) , InfixL ( operator ">>" >> return RightShift ) ] , [ InfixL ( operator "==" >> return Equal ) , InfixL ( operator "!=" >> return Different ) ] , [ InfixL ( operator "=~" >> return RegexMatch ) , InfixL ( operator "!~" >> return NotRegexMatch ) ] , [ InfixL ( operator ">=" >> return MoreEqualThan ) , InfixL ( operator "<=" >> return LessEqualThan ) , InfixL ( operator ">" >> return MoreThan ) , InfixL ( operator "<" >> return LessThan ) ] , [ InfixL ( reserved "and" >> return And ) , InfixL ( reserved "or" >> return Or ) ] ] indexLookupChain :: Parser (Expression -> Expression) indexLookupChain = List.foldr1 (flip (.)) <$> some checkLookup where checkLookup = flip Lookup <$> between (operator "[") (operator "]") expression stringExpression :: Parser Expression stringExpression = fmap (Terminal . UInterpolable) interpolableString <|> (reserved "undef" *> return (Terminal UUndef)) <|> fmap (Terminal . UBoolean) puppetBool <|> variable <|> fmap Terminal literalValue varAssign :: Parser VarAssignDecl varAssign = do p <- getPosition mt <- optional datatype v <- variableReference void $ symbolic '=' e <- expression when (Text.all Char.isDigit v) (fail "Can't assign fully numeric variables") pe <- getPosition return (VarAssignDecl mt v e (p :!: pe)) nodeDecl :: Parser [NodeDecl] nodeDecl = do p <- getPosition reserved "node" let toString (UString s) = s toString (UNumber n) = scientific2text n toString _ = panic "Can't happen at nodeDecl" nodename = (reserved "default" >> return NodeDefault) <|> fmap (NodeName . toString) literalValue ns <- (fmap NodeMatch termRegexp <|> nodename) `sepBy1` comma inheritance <- option S.Nothing (fmap S.Just (reserved "inherits" *> nodename)) st <- braces statementList pe <- getPosition return [NodeDecl n st inheritance (p :!: pe) | n <- ns] defineDecl :: Parser DefineDecl defineDecl = do p <- getPosition reserved "define" name <- typeName -- TODO check native type params <- option V.empty puppetClassParameters st <- braces statementList pe <- getPosition return (DefineDecl name params st (p :!: pe)) puppetClassParameters :: Parser (Vector (Pair (Pair Text (S.Maybe UDataType)) (S.Maybe Expression))) puppetClassParameters = V.fromList <$> parens (sepComma var) where toStrictMaybe (Just x) = S.Just x toStrictMaybe Nothing = S.Nothing var :: Parser (Pair (Pair Text (S.Maybe UDataType)) (S.Maybe Expression)) var = do tp <- toStrictMaybe <$> optional datatype n <- variableReference df <- toStrictMaybe <$> optional (symbolic '=' *> expression) return (n :!: tp :!: df) puppetIfStyleCondition :: Parser (Pair Expression (Vector Statement)) puppetIfStyleCondition = (:!:) <$> expression <*> braces statementList unlessCondition :: Parser ConditionalDecl unlessCondition = do p <- getPosition reserved "unless" (cond :!: stmts) <- puppetIfStyleCondition pe <- getPosition return (ConditionalDecl (V.singleton (Not cond :!: stmts)) (p :!: pe)) ifCondition :: Parser ConditionalDecl ifCondition = do p <- getPosition reserved "if" maincond <- puppetIfStyleCondition others <- many (reserved "elsif" *> puppetIfStyleCondition) elsecond <- option V.empty (reserved "else" *> braces statementList) let ec = if V.null elsecond then [] else [Terminal (UBoolean True) :!: elsecond] pe <- getPosition return (ConditionalDecl (V.fromList (maincond : others ++ ec)) (p :!: pe)) caseCondition :: Parser ConditionalDecl caseCondition = do let puppetRegexpCase = Terminal . URegexp <$> termRegexp defaultCase = Terminal (UBoolean True) <$ try (reserved "default") matchesToExpression e (x, stmts) = f x :!: stmts where f = case x of (Terminal (UBoolean _)) -> identity (Terminal (URegexp _)) -> RegexMatch e _ -> Equal e cases = do matches <- (puppetRegexpCase <|> defaultCase <|> expression) `sepBy1` comma void $ symbolic ':' stmts <- braces statementList return $ map (,stmts) matches p <- getPosition reserved "case" expr1 <- expression condlist <- concat <$> braces (some cases) pe <- getPosition return (ConditionalDecl (V.fromList (map (matchesToExpression expr1) condlist)) (p :!: pe) ) data OperatorChain a = OperatorChain a LinkType (OperatorChain a) | EndOfChain a instance Foldable OperatorChain where foldMap f (EndOfChain x) = f x foldMap f (OperatorChain a _ nx) = f a <> foldMap f nx operatorChainStatement :: OperatorChain a -> a operatorChainStatement (OperatorChain a _ _) = a operatorChainStatement (EndOfChain x) = x zipChain :: OperatorChain a -> [ ( a, a, LinkType ) ] zipChain (OperatorChain a d nx) = (a, operatorChainStatement nx, d) : zipChain nx zipChain (EndOfChain _) = [] depOperator :: Parser LinkType depOperator = (operator "->" *> pure RBefore) <|> (operator "~>" *> pure RNotify) assignment :: Parser AttributeDecl assignment = (AttributeDecl <$> key <*> arrowOp <*> expression) <|> (AttributeWildcard <$> (symbolic '*' *> symbol "=>" *> expression)) where key = identl (satisfy Char.isAsciiLower) (satisfy acceptable) "Assignment key" acceptable x = Char.isAsciiLower x || Char.isAsciiUpper x || Char.isDigit x || (x == '_') || (x == '-') arrowOp = (symbol "=>" *> pure AssignArrow) <|> (symbol "+>" *> pure AppendArrow) searchExpression :: Parser SearchExpression searchExpression = makeExprParser (lexeme searchterm) searchTable where searchTable :: [[Operator Parser SearchExpression]] searchTable = [ [ InfixL ( reserved "and" >> return AndSearch ) , InfixL ( reserved "or" >> return OrSearch ) ] ] searchterm = parens searchExpression <|> check check = do attrib <- parameterName opr <- (operator "==" *> return EqualitySearch) <|> (operator "!=" *> return NonEqualitySearch) term <- stringExpression return (opr attrib term) resCollDecl :: Position -> Text -> Parser ResCollDecl resCollDecl p restype = do openchev <- some (char '<') when (length openchev > 2) (fail "Too many brackets") void $ symbolic '|' e <- option AlwaysTrue searchExpression void (char '|') void (count (length openchev) (char '>')) sc overrides <- option [] $ braces (sepComma assignment) let collectortype = if length openchev == 1 then Collector else ExportedCollector pe <- getPosition return (ResCollDecl collectortype restype e (V.fromList overrides) (p :!: pe) ) classDecl :: Parser ClassDecl classDecl = do p <- getPosition reserved "class" ClassDecl <$> className <*> option V.empty puppetClassParameters <*> option S.Nothing (fmap S.Just (reserved "inherits" *> className)) <*> braces statementList <*> ( (p :!:) <$> getPosition ) mainFuncDecl :: Parser MainFuncDecl mainFuncDecl = do p <- getPosition (fname, args) <- genFunctionCall True pe <- getPosition return (MainFuncDecl fname args (p :!: pe)) hoLambdaDecl :: Parser HigherOrderLambdaDecl hoLambdaDecl = do p <- getPosition fc <- try lambdaCall pe <- getPosition return (HigherOrderLambdaDecl fc (p :!: pe)) dotLambdaDecl :: Parser HigherOrderLambdaDecl dotLambdaDecl = do p <- getPosition ex <- expression pe <- getPosition hf <- case ex of FunctionApplication e (Terminal (UHOLambdaCall hf)) -> do unless (S.isNothing (hf ^. hoLambdaExpr)) (fail "Can't call a function with . and ()") return (hf & hoLambdaExpr .~ S.Just e) Terminal (UHOLambdaCall hf) -> do when (S.isNothing (hf ^. hoLambdaExpr)) (fail "This function needs data to operate on") return hf _ -> fail "A method chained by dots." unless (hf ^. hoLambdaFunc == LambEach) (fail "Expected 'each', the other types of method calls are not supported by language-puppet at the statement level.") return (HigherOrderLambdaDecl hf (p :!: pe)) resDefaultDecl :: Parser ResDefaultDecl resDefaultDecl = do p <- getPosition rnd <- resourceNameRef let assignmentList = V.fromList <$> sepComma1 assignment asl <- braces assignmentList pe <- getPosition return (ResDefaultDecl rnd asl (p :!: pe)) resOverrideDecl :: Parser [ResOverrideDecl] resOverrideDecl = do p <- getPosition restype <- resourceNameRef names <- brackets (expression `sepBy1` comma) "Resource reference values" assignments <- V.fromList <$> braces (sepComma assignment) pe <- getPosition return [ ResOverrideDecl restype n assignments (p :!: pe) | n <- names ] -- | Heterogeneous chain (interleaving resource declarations with -- resource references) needs to be supported: -- -- class { 'docker::service': } -> -- Class['docker'] chainableResources :: Parser [Statement] chainableResources = do let withresname = do p <- getPosition restype <- resourceNameRef lookAhead anyChar >>= \x -> case x of '[' -> do resnames <- brackets (expression `sepBy1` comma) pe <- getPosition pure (ChainResRefr restype resnames (p :!: pe)) _ -> ChainResColl <$> resCollDecl p restype chain <- parseRelationships $ pure <$> try withresname <|> map ChainResDecl <$> resDeclGroup let relations = do (g1, g2, lt) <- zipChain chain (rt1, rn1, _ :!: pe1) <- concatMap extractResRef g1 (rt2, rn2, ps2 :!: _ ) <- concatMap extractResRef g2 return (DepDecl (rt1 :!: rn1) (rt2 :!: rn2) lt (pe1 :!: ps2)) return $ map DependencyDeclaration relations <> (chain ^.. folded . folded . to extractChainStatement . folded) where extractResRef :: ChainableRes -> [(Text, Expression, PPosition)] extractResRef (ChainResColl _) = [] extractResRef (ChainResDecl (ResDecl rt rn _ _ pp)) = [(rt,rn,pp)] extractResRef (ChainResRefr rt rns pp) = [(rt,rn,pp) | rn <- rns] extractChainStatement :: ChainableRes -> [Statement] extractChainStatement (ChainResColl r) = [ResourceCollectionDeclaration r] extractChainStatement (ChainResDecl d) = [ResourceDeclaration d] extractChainStatement ChainResRefr{} = [] parseRelationships :: Parser a -> Parser (OperatorChain a) parseRelationships p = do g <- p o <- optional depOperator case o of Just o' -> OperatorChain g o' <$> parseRelationships p Nothing -> pure (EndOfChain g) resDeclGroup :: Parser [ResDecl] resDeclGroup = do let resourceName = expression resourceDeclaration = do p <- getPosition names <- brackets (sepComma1 resourceName) <|> fmap return resourceName void $ symbolic ':' vals <- fmap V.fromList (sepComma assignment) pe <- getPosition return [(n, vals, p :!: pe) | n <- names ] groupDeclaration = (,) <$> many (char '@') <*> typeName <* symbolic '{' (virts, rtype) <- try groupDeclaration -- for matching reasons, this gets a try until the opening brace let sep = symbolic ';' <|> comma x <- resourceDeclaration `sepEndBy1` sep void $ symbolic '}' virtuality <- case virts of "" -> return Normal "@" -> return Virtual "@@" -> return Exported _ -> fail "Invalid virtuality" return [ ResDecl rtype rname conts virtuality pos | (rname, conts, pos) <- concat x ] statement :: Parser [Statement] statement = (pure . HigherOrderLambdaDeclaration <$> try dotLambdaDecl) <|> (pure . VarAssignmentDeclaration <$> varAssign) <|> (map NodeDeclaration <$> nodeDecl) <|> (pure . DefineDeclaration <$> defineDecl) <|> (pure . ConditionalDeclaration <$> unlessCondition) <|> (pure . ConditionalDeclaration <$> ifCondition) <|> (pure . ConditionalDeclaration <$> caseCondition) <|> (pure . ResourceDefaultDeclaration <$> try resDefaultDecl) <|> (map ResourceOverrideDeclaration <$> try resOverrideDecl) <|> chainableResources <|> (pure . ClassDeclaration <$> classDecl) <|> (pure . HigherOrderLambdaDeclaration <$> hoLambdaDecl) <|> (pure . MainFunctionDeclaration <$> mainFuncDecl) "Statement" datatype :: Parser UDataType datatype = dtString <|> dtInteger <|> dtFloat <|> dtNumeric <|> (UDTBoolean <$ reserved "Boolean") <|> (UDTScalar <$ reserved "Scalar") <|> (UDTData <$ reserved "Data") <|> (UDTAny <$ reserved "Any") <|> (UDTCollection <$ reserved "Collection") <|> dtArray <|> dtHash <|> (UDTUndef <$ reserved "Undef") <|> (reserved "Optional" *> (UDTOptional <$> brackets datatype)) <|> (UNotUndef <$ reserved "NotUndef") <|> (reserved "Variant" *> (UDTVariant . NE.fromList <$> brackets (datatype `sepBy1` symbolic ','))) -- while all the other cases are straightforward, it seems that the -- following syntax is a valid regexp for puppet: -- '^dqsqsdqs$' -- instead of: -- /^dqsqsdqs$/ -- -- That is the reason there is a "quotedRegexp" case <|> (reserved "Pattern" *> (UDTPattern . NE.fromList <$> brackets ( (termRegexp <|> quotedRegexp) `sepBy1` symbolic ','))) <|> (reserved "Enum" *> (UDTEnum . NE.fromList <$> brackets (expression `sepBy1` symbolic ','))) <|> dtExternal "UDataType" where quotedRegexp = stringLiteral' >>= compileRegexp integer = integerOrDouble >>= either (return . fromIntegral) (\d -> fail ("Integer value expected, instead of " ++ show d)) float = either fromIntegral identity <$> integerOrDouble dtArgs str def parseArgs = do void $ reserved str fromMaybe def <$> optional (brackets parseArgs) dtbounded s constructor parser = dtArgs s (constructor Nothing Nothing) $ do lst <- parser `sepBy1` symbolic ',' case lst of [minlen] -> return $ constructor (Just minlen) Nothing [minlen,maxlen] -> return $ constructor (Just minlen) (Just maxlen) _ -> fail ("Too many arguments to datatype " ++ Text.unpack s) dtString = dtbounded "String" UDTString integer dtInteger = dtbounded "Integer" UDTInteger integer dtFloat = dtbounded "Float" UDTFloat float dtNumeric = dtbounded "Numeric" (\ma mb -> UDTVariant (UDTFloat ma mb :| [UDTInteger (truncate <$> ma) (truncate <$> mb)])) float dtArray = do reserved "Array" ml <- optional $ brackets $ do tp <- datatype rst <- optional (symbolic ',' *> integer `sepBy1` symbolic ',') return (tp, rst) case ml of Nothing -> return (UDTArray UDTData 0 Nothing) Just (t, Nothing) -> return (UDTArray t 0 Nothing) Just (t, Just [mi]) -> return (UDTArray t mi Nothing) Just (t, Just [mi, mx]) -> return (UDTArray t mi (Just mx)) Just (_, Just _) -> fail "Too many arguments to datatype Array" dtHash = do reserved "Hash" ml <- optional $ brackets $ do tk <- datatype symbolic ',' tv <- datatype rst <- optional (symbolic ',' *> integer `sepBy1` symbolic ',') return (tk, tv, rst) case ml of Nothing -> return (UDTHash UDTScalar UDTData 0 Nothing) Just (tk, tv, Nothing) -> return (UDTHash tk tv 0 Nothing) Just (tk, tv, Just [mi]) -> return (UDTHash tk tv mi Nothing) Just (tk, tv, Just [mi, mx]) -> return (UDTHash tk tv mi (Just mx)) Just (_, _, Just _) -> fail "Too many arguments to datatype Hash" dtExternal = reserved "Stdlib::HTTPUrl" $> UDTData <|> reserved "Stdlib::Absolutepath" $> UDTData <|> reserved "Stdlib::Unixpath" $> UDTData <|> reserved "Nginx::ErrorLogSeverity" $> UDTData <|> reserved "Jenkins::Tunnel" $> UDTData <|> reserved "Systemd::Unit" $> UDTData <|> reserved "Systemd::ServiceLimits" $> UDTData <|> reserved "Systemd::Dropin" $> UDTData statementList :: Parser (Vector Statement) statementList = (V.fromList . concat) <$> many statement lambdaCall :: Parser HOLambdaCall lambdaCall = do let tostrict (Just x) = S.Just x tostrict Nothing = S.Nothing HOLambdaCall <$> lambFunc <*> fmap (tostrict . join) (optional (parens (optional expression))) <*> lambParams <*> (symbolic '{' *> fmap (V.fromList . concat) (many (try statement))) <*> fmap tostrict (optional expression) <* symbolic '}' where lambFunc :: Parser LambdaFunc lambFunc = (reserved "each" *> pure LambEach) <|> (reserved "map" *> pure LambMap ) <|> (reserved "reduce" *> pure LambReduce) <|> (reserved "filter" *> pure LambFilter) <|> (reserved "slice" *> pure LambSlice) <|> (reserved "lookup" *> pure LambLookup) lambParams :: Parser LambdaParameters lambParams = between (symbolic '|') (symbolic '|') hp where acceptablePart = Text.pack <$> identifier lambdaParameter :: Parser LambdaParameter lambdaParameter = LParam <$> optional datatype <*> (char '$' *> acceptablePart) hp = do vars <- lambdaParameter `sepBy1` comma case vars of [a] -> return (BPSingle a) [a,b] -> return (BPPair a b) _ -> fail "Invalid number of variables between the pipes"