language-puppet 0.1.7.2 → 0.1.8.0
raw patch · 9 files changed
+233/−52 lines, 9 filesdep +failure
Dependencies added: failure
Files
- Erb/Compute.hs +2/−1
- Puppet/Daemon.hs +11/−1
- Puppet/Init.hs +3/−2
- Puppet/Interpreter/Catalog.hs +74/−16
- Puppet/Interpreter/Types.hs +8/−4
- PuppetDB/Query.hs +67/−0
- PuppetDB/Rest.hs +64/−24
- language-puppet.cabal +3/−3
- ruby/calcerb.rb +1/−1
Erb/Compute.hs view
@@ -18,7 +18,7 @@ type TemplateAnswer = Either String String initTemplateDaemon :: Prefs -> IO (String -> String -> [(String, GeneralValue)] -> IO (Either String String))-initTemplateDaemon (Prefs _ modpath templatepath _ _) = do+initTemplateDaemon (Prefs _ modpath templatepath _ _ _) = do controlchan <- newChan forkIO (templateDaemon modpath templatepath controlchan) return (templateQuery controlchan)@@ -77,4 +77,5 @@ toRuby' (ResolvedBool False) = "false" toRuby' (ResolvedArray rr) = "[" ++ intercalate ", " (map toRuby' rr) ++ "]" toRuby' (ResolvedHash hh) = "{ " ++ intercalate ", " (map (\(varname, varval) -> show varname ++ " => " ++ toRuby' varval) hh) ++ " }"+toRuby' ResolvedUndefined = ":undef" toRuby' x = show x
Puppet/Daemon.hs view
@@ -19,6 +19,7 @@ import qualified Data.List.Utils as DLU import qualified Data.Map as Map import Text.Parsec.Pos (initialPos)+import PuppetDB.Rest -- this daemon returns a catalog when asked for a node and facts data DaemonMessage@@ -50,6 +51,12 @@ It is recommended to ask for as many parser and interpreter threads as there are CPUs. +It can optionnaly talk with PuppetDB, by setting an URL in the 'Prefs' data+structure. The recommended way to set it to http://localhost:8080 and set a SSH+tunnel :++> ssh -L 8080:localhost:8080 puppet.host+ Known bugs : * It might be buggy when top level statements that are not class/define/nodes@@ -86,7 +93,10 @@ case message of QCatalog (nodename, facts, respchan) -> do logDebug ("Received query for node " ++ nodename)- (stmts, warnings) <- getCatalog getstmts gettemplate nodename facts+ let pdbfunc = case (puppetDBurl prefs) of+ Just x -> Just (pdbResRequest x)+ Nothing -> Nothing+ (stmts, warnings) <- getCatalog getstmts gettemplate pdbfunc nodename facts mapM_ logWarning warnings case stmts of Left x -> writeChan respchan (RCatalog $ Left x)
Puppet/Init.hs view
@@ -9,14 +9,15 @@ modules :: FilePath, -- ^ The path to the modules. templates :: FilePath, -- ^ The path to the template. compilepoolsize :: Int, -- ^ Size of the compiler pool.- parsepoolsize :: Int -- ^ Size of the parser pool.+ parsepoolsize :: Int, -- ^ Size of the parser pool.+ puppetDBurl :: Maybe String -- ^ Url of the PuppetDB connector (must be cleartext). } deriving (Show) -- | Generates the 'Prefs' structure from a single path. -- -- > genPrefs "/etc/puppet" genPrefs :: String -> Prefs-genPrefs basedir = Prefs (basedir ++ "/manifests") (basedir ++ "/modules") (basedir ++ "/templates") 1 1+genPrefs basedir = Prefs (basedir ++ "/manifests") (basedir ++ "/modules") (basedir ++ "/templates") 1 1 Nothing -- | Generates 'Facts' from pairs of strings. --
Puppet/Interpreter/Catalog.hs view
@@ -38,11 +38,12 @@ import Puppet.Interpreter.Functions import Puppet.Interpreter.Types import Puppet.Printers+import qualified PuppetDB.Query as PDB import System.IO.Unsafe import Data.List import Data.Char (isDigit,toLower,toUpper, isAlpha, isAlphaNum, isSpace)-import Data.Maybe (isJust, fromJust, catMaybes)+import Data.Maybe (isJust, fromJust, catMaybes, isNothing) import Data.Either (lefts, rights, partitionEithers) import Data.Ord (comparing) import Text.Parsec.Pos@@ -78,14 +79,33 @@ -> (String -> String -> [(String, GeneralValue)] -> IO (Either String String)) -- ^ The \"get template\" function. Given a file name, a scope name and a -- list of variables, it should return the computed template.+ -> Maybe (String -> PDB.Query -> IO (Either String [CResource]))+ -- ^ The \"puppetDB Rest API\" function. Given the machine fqdn, a request+ -- type (resources, nodes, facts, ..) and a query, it returns a+ -- ResolvedValue, or some error. -> String -- ^ Name of the node. -> Facts -- ^ Facts of this node. -> IO (Either String FinalCatalog, [String])-getCatalog getstatements gettemplate nodename facts = do+getCatalog getstatements gettemplate puppetdb nodename facts = do let convertedfacts = Map.map (\fval -> (Right fval, initialPos "FACTS")) facts- (output, finalstate) <- runStateT ( runErrorT ( computeCatalog getstatements nodename ) ) (ScopeState [["::"]] convertedfacts Map.empty [] 1 (initialPos "dummy") Map.empty getstatements [] [] [] gettemplate)+ (output, finalstate) <- runStateT ( runErrorT ( computeCatalog getstatements nodename ) )+ (ScopeState+ { curScope = [["::"]]+ , curVariables = convertedfacts+ , curClasses = Map.empty+ , curDefaults = []+ , curResId = 1+ , curPos = (initialPos "dummy")+ , nestedtoplevels = Map.empty+ , getStatementsFunction = getstatements+ , getWarnings = []+ , curCollect = []+ , unresolvedRels = []+ , computeTemplateFunction = gettemplate+ , puppetDBFunction = puppetdb+ } ) case output of Left x -> return (Left x, getWarnings finalstate) Right _ -> return (output, getWarnings finalstate)@@ -130,7 +150,7 @@ else do -- Note that amending attributes with a collector does collect virtual -- values. Hence no filtering on the collectors is done here.- isCollected <- liftM curCollect get >>= mapM (\(x, _) -> x res)+ isCollected <- liftM curCollect get >>= mapM (\(x, _, _) -> x res) case (or isCollected, crvirtuality res) of (True, Exported) -> return [res { crvirtuality = Normal }, res] (True, _) -> return [res { crvirtuality = Normal } ]@@ -138,9 +158,9 @@ processOverride :: CResource -> Map.Map String ResolvedValue -> CatalogMonad (Map.Map String ResolvedValue) processOverride cr prms =- let applyOverride :: CResource -> Map.Map String ResolvedValue -> (CResource -> CatalogMonad Bool, [(GeneralString, GeneralValue)]) -> CatalogMonad (Map.Map String ResolvedValue)+ let applyOverride :: CResource -> Map.Map String ResolvedValue -> (CResource -> CatalogMonad Bool, [(GeneralString, GeneralValue)], Maybe PDB.Query) -> CatalogMonad (Map.Map String ResolvedValue) -- this checks if the collection function matches- applyOverride c prm (func, overs) = do+ applyOverride c prm (func, overs, _) = do check <- func c if check then foldM tryReplace prm overs@@ -153,13 +173,37 @@ rv <- resolveGeneralValue gv return $ Map.insert rs rv curmap -- Collectors are filtered so that only those with overrides are passed to the fold.- in liftM (filter (not . null . snd) . curCollect) get >>= foldM (applyOverride cr) prms+ in liftM (filter (\(_, x, _) -> not $ null x) . curCollect) get >>= foldM (applyOverride cr) prms +retrieveRemoteResources :: (PDB.Query -> IO (Either String [CResource])) -> PDB.Query -> CatalogMonad [CResource]+retrieveRemoteResources f q = do+ res <- liftIO $ f q+ hashes <- case res of+ Right h -> return h+ Left err -> throwError $ "PuppetDB error: " ++ err+ return hashes +extractRelations :: CResource -> CatalogMonad CResource+extractRelations cr = do+ let (params, rels) = partitionParamsRelations (crparams cr)+ -- TODO export relations+ return cr { crparams = params }+ finalResolution :: Catalog -> CatalogMonad FinalCatalog finalResolution cat = do- --liftIO $ putStrLn $ "FINAL RESOLUTION"- collected <- mapM collectionChecks cat >>= mapM evaluateDefine . concat+ pdbfunction <- fmap puppetDBFunction get+ fqdnr <- getVariable "::fqdn"+ collectedRemote <- case pdbfunction of+ Just f -> do+ fqdn <- case fqdnr of+ Just (Right (ResolvedString f), _) -> return f+ _ -> throwError "Could not get FQDN during final resolution"+ remoteCollects <- fmap (catMaybes . map (\(_,_,x) -> x) . curCollect) get+ fmap concat (mapM (retrieveRemoteResources (f fqdn)) remoteCollects)+ Nothing -> return []+ collectedRemote' <- mapM extractRelations collectedRemote+ collectedLocal <- fmap concat (mapM collectionChecks cat)+ collected <- mapM evaluateDefine (collectedLocal ++ collectedRemote') let (real, allvirtual) = partition (\x -> crvirtuality x == Normal) (concat collected) (_, exported) = partition (\x -> crvirtuality x == Virtual) allvirtual --export stuff@@ -231,7 +275,7 @@ nstate = curstate { nestedtoplevels = ntop } put nstate addWarning = modify . pushWarning-addCollect = modify . pushCollect+addCollect ((func, query), overrides) = modify $ pushCollect (func, overrides, query) -- this pushes the relations only if they exist -- the parameter is of the form -- ( [dstrelations], srcresource, type, pos )@@ -326,16 +370,23 @@ evaluateDefine r@(CResource _ rname rtype rparams rvirtuality rpos) = let evaluateDefineDeclaration dtype args dstmts dpos = do --oldpos <- getPos- setPos dpos pushScope ["#DEFINE#" ++ dtype] -- add variables mrrparams <- mapM (\(gs, gv) -> do { rgs <- resolveGeneralString gs; rgv <- tryResolveGeneralValue gv; return (rgs, (rgv, dpos)); }) rparams let expr = gs2gv rname mparams = Map.fromList mrrparams+ defineparamset = Set.fromList $ map fst args+ mandatoryparams = Set.fromList $ map fst $ filter (isNothing . snd) args+ resourceparamset = Set.fromList $ map fst mrrparams+ extraparams = Set.difference resourceparamset (Set.union defineparamset metaparameters)+ unsetparams = Set.difference mandatoryparams resourceparamset+ unless (Set.null extraparams) $ throwPosError $ "Spurious parameters set for " ++ dtype ++ ": " ++ intercalate ", " (Set.toList extraparams)+ unless (Set.null unsetparams) $ throwPosError $ "Unset parameters set for " ++ dtype ++ ": " ++ intercalate ", " (Set.toList unsetparams) putVariable "title" (expr, rpos) putVariable "name" (expr, rpos) mapM_ (loadClassVariable rpos mparams) args + setPos dpos -- parse statements res <- mapM evaluateStatements dstmts nres <- handleDelayedActions (concat res)@@ -903,7 +954,7 @@ myfunction (CResource _ mcrname mcrtype _ _ _) = do srname <- resolveGeneralString mcrname return ((srname == rname) && (mcrtype == rtype))- addCollect (myfunction, [])+ addCollect ((myfunction, Just $ PDB.queryRealize rtype rname) , []) return () pushRealize (ResolvedRReference _ x) = throwPosError (show x ++ " was not resolved to a string") pushRealize x = throwPosError ("A reference was expected instead of " ++ show x)@@ -1022,10 +1073,10 @@ gs2gv (Left e) = Left e gs2gv (Right s) = Right $ ResolvedString s -collectionFunction :: Virtuality -> String -> Expression -> CatalogMonad (CResource -> CatalogMonad Bool)+collectionFunction :: Virtuality -> String -> Expression -> CatalogMonad (CResource -> CatalogMonad Bool, Maybe PDB.Query) collectionFunction virt mrtype exprs = do- finalfunc <- case exprs of- BTrue -> return (\_ -> return True)+ (finalfunc, pdbquery) <- case exprs of+ BTrue -> return (\_ -> return True, Just (PDB.collectAll mrtype)) EqualOperation a b -> do ra <- resolveExpression a rb <- resolveExpression b@@ -1052,6 +1103,10 @@ let filtered = filter (compareRValues rb) xs in return $ not $ null filtered _ -> return $ compareRValues cmp rb+ , case (paramname, rb) of+ ("tag", ResolvedString tagval) -> Just (PDB.collectTag mrtype tagval)+ (param, ResolvedString prmval) -> Just (PDB.collectParam mrtype param prmval)+ _ -> Nothing ) x -> throwPosError $ "TODO : implement collection function for " ++ show x return (\res -> do@@ -1059,7 +1114,10 @@ if (crtype res == mrtype) && ( ((virt == Virtual) && (crvirtuality res == Normal)) || (crvirtuality res == virt)) then finalfunc res else return False- )+ , if (virt == Exported)+ then pdbquery+ else Nothing+ ) generalValue2Expression :: GeneralValue -> Expression
Puppet/Interpreter/Types.hs view
@@ -1,6 +1,7 @@ module Puppet.Interpreter.Types where import Puppet.DSL.Types+import qualified PuppetDB.Query as PDB import Text.Parsec.Pos import Control.Monad.State import Control.Monad.Error@@ -118,17 +119,20 @@ -- ^ This is a function that, given the type of a top level statement and -- its name, should return it. getWarnings :: ![String], -- ^ List of warnings.- curCollect :: ![(CResource -> CatalogMonad Bool, [(GeneralString, GeneralValue)])],+ curCollect :: ![(CResource -> CatalogMonad Bool, [(GeneralString, GeneralValue)], Maybe PDB.Query)], -- ^ A bit complicated, this stores the collection functions. These are -- functions that determine whether a resource should be collected or not. -- It can optionally store overrides, which will be applied in the end on- -- all resources.+ -- all resources. It can also store a PuppetDB query. unresolvedRels :: ![([(LinkType, GeneralValue, GeneralValue)], (String, GeneralString), RelUpdateType, SourcePos)], -- ^ This stores unresolved relationships, because the original string name -- can't be resolved. Fieds are [ ( [dstrelations], srcresource, type, pos ) ]- computeTemplateFunction :: String -> String -> [(String, GeneralValue)] -> IO (Either String String)+ computeTemplateFunction :: String -> String -> [(String, GeneralValue)] -> IO (Either String String), -- ^ Function that takes a filename, the current scope and a list of -- variables. It returns an error or the computed template.+ puppetDBFunction :: Maybe (String -> PDB.Query -> IO (Either String [CResource]))+ -- ^ Function that takes a fqdn, request type (resources, nodes, facts, ..)+ -- and a query, and returns a resolved value from puppetDB. } -- | The monad all the interpreter lives in. It is 'ErrorT' with a state.@@ -144,7 +148,7 @@ generalizeStringS = Right -- |This is the set of meta parameters-metaparameters = Set.fromList ["tag","stage","name","title","alias","audit","check","loglevel","noop","schedule"]+metaparameters = Set.fromList ["tag","stage","name","title","alias","audit","check","loglevel","noop","schedule", "EXPORTEDSOURCE"] getPos = liftM curPos get modifyScope f sc = sc { curScope = f $ curScope sc }
+ PuppetDB/Query.hs view
@@ -0,0 +1,67 @@+module PuppetDB.Query where++import Data.List (intercalate)+import Data.Char (toUpper)+import Data.String.Utils (split)++data Operator = OEqual | OOver | OUnder | OOverE | OUnderE | OAnd | OOr | ONot+ deriving (Show, Ord, Eq)++-- [Field] Value+data Query = Query Operator [Query] | Term String | Terms [String]+ deriving (Show, Ord, Eq)++capitalize :: String -> String+capitalize = intercalate "::" . map capitalize' . split "::"++capitalize' :: String -> String+capitalize' "" = ""+capitalize' (x:xs) = toUpper x : xs++-- | Query used for realizing a resources, when its type and title is known.+queryRealize :: String -> String -> Query+queryRealize rtype rtitle = Query OAnd+ [ Query OEqual [ Term "type", Term (capitalize rtype) ]+ , Query OEqual [ Term "title", Term rtitle ]+ , Query OEqual [ Term "exported", Term "true" ]+ ]++-- | Collects all resources of a given type+collectAll :: String -> Query+collectAll rtype = Query OAnd [ Query OEqual [ Term "type", Term (capitalize rtype) ]+ , Query OEqual [ Term "exported", Term "true" ]+ ]++-- | Collections based on tags.+collectTag :: String -> String -> Query+collectTag rtype tagval = Query OAnd+ [ Query OEqual [ Term "type", Term (capitalize rtype) ]+ , Query OEqual [ Term "tag" , Term tagval ]+ , Query OEqual [ Term "exported", Term "true" ]+ ]++-- | Used to emulate collections such as `Type<|| prmname == "prmval" ||>`+collectParam :: String -> String -> String -> Query+collectParam rtype prmname prmval = Query OAnd+ [ Query OEqual [ Term "type", Term (capitalize rtype) ]+ , Query OEqual [ Terms ["parameter", "prmname"], Term prmval ]+ , Query OEqual [ Term "exported", Term "true" ]+ ]++dq :: String -> String+dq x = '"' : x ++ "\""++showOperator :: Operator -> String+showOperator OEqual = dq "="+showOperator OOver = dq ">"+showOperator OUnder = dq "<"+showOperator OOverE = dq ">="+showOperator OUnderE = dq "<="+showOperator OAnd = dq "and"+showOperator OOr = dq "or"+showOperator ONot = dq "not"++showQuery :: Query -> String+showQuery (Query op subqueries) = "[" ++ intercalate ", " (showOperator op : map showQuery subqueries) ++ "]"+showQuery (Term t) = show t+showQuery (Terms ts) = show ts
PuppetDB/Rest.hs view
@@ -2,8 +2,9 @@ module PuppetDB.Rest where +import qualified Puppet.DSL.Types as DT import Puppet.Interpreter.Types-import Puppet.Printers+import qualified PuppetDB.Query as PDB import Network.HTTP.Conduit import qualified Network.HTTP.Types as W@@ -15,6 +16,10 @@ import qualified Data.Text as T import Data.Attoparsec.Number import qualified Codec.Text.IConv as IConv+import qualified Control.Exception as X+import Control.Monad.Error+import Control.Applicative+import qualified Text.Parsec.Pos as TPP instance FromJSON ResolvedValue where parseJSON Null = return ResolvedUndefined@@ -29,29 +34,64 @@ ) (HM.toList o)) parseJSON (Bool b) = return $ ResolvedBool b -toAndQuery = undefined+instance FromJSON CResource where+ parseJSON (Object o) = do+ utitle <- o .: "title"+ params <- o .: "parameters"+ sourcefile <- o .: "sourcefile"+ sourceline <- o .: "sourceline"+ certname <- o .: "certname"+ let _ = params :: HM.HashMap String ResolvedValue+ parameters = map (\(k,v) -> (Right k, Right v)) $ ("EXPORTEDSOURCE", ResolvedString certname) : HM.toList params :: [(GeneralString, GeneralValue)]+ position = TPP.newPos (sourcefile ++ "(host: " ++ certname ++ ")") sourceline 1+ CResource <$> pure 0+ <*> pure (Right utitle)+ <*> fmap (T.unpack . T.toLower) (o .: "type")+ <*> pure parameters+ <*> pure DT.Normal+ <*> pure position+ parseJSON _ = mzero -simpleNodeQuery :: String -> [(String, String)] -> IO (Either String ResolvedValue)-simpleNodeQuery url query = rawRequest url "nodes" (toAndQuery query)+runRequest req = do+ let doRequest = withManager (\manager -> fmap responseBody $ httpLbs req manager) :: IO L.ByteString+ eHandler :: X.SomeException -> IO (Either String L.ByteString)+ eHandler e = return $ Left $ show e ++ ", with queryString " ++ (BC.unpack $ queryString req)+ mo <- liftIO ((fmap Right doRequest) `X.catch` eHandler)+ case mo of+ Right o -> do+ let utf8 = IConv.convert "LATIN1" "UTF-8" o+ case decode' utf8 of+ Just x -> return x+ Nothing -> throwError "Json decoding has failed"+ Left err -> throwError err -simpleResourceQuery :: String -> [(String, String)] -> IO (Either String ResolvedValue)-simpleResourceQuery url query = rawRequest url "resources" (toAndQuery query)+isNotLocal :: String -> CResource -> Bool+isNotLocal fqdn cr =+ let prms = crparams cr+ e = filter ((== Right "EXPORTEDSOURCE") . fst ) prms :: [(GeneralString, GeneralValue)]+ in case e of+ [(_, Right (ResolvedString x))] -> x /= fqdn+ _ -> True -rawRequest :: String -> String -> String -> IO (Either String ResolvedValue)-rawRequest url querytype query = do- let q = BC.unpack $ W.renderSimpleQuery False [("query", BC.pack query)]- pfunc = parseUrl (url ++ "/" ++ querytype ++ "?" ++ q)- eInitReq <- case querytype of- "resources" -> fmap Right pfunc- "nodes" -> fmap Right pfunc- _ -> return $ Left $ "Invalid query type " ++ querytype- case eInitReq of- Right initReq -> do- let req = initReq { requestHeaders = [("Accept", "application/json")] }- o <- withManager (\manager -> fmap responseBody $ httpLbs req manager) :: IO L.ByteString- let utf8 = IConv.convert "LATIN1" "UTF-8" o- case decode' utf8 :: Maybe ResolvedValue of- Just x@(ResolvedArray _) -> return $ Right x- Just x -> return $ Left $ "PuppetDB should have returned an array, not " ++ showValue x- Nothing -> return $ Left "Json decoding has failed"- Left err -> return $ Left err+pdbResRequest :: String -> String -> PDB.Query -> IO (Either String [CResource])+pdbResRequest url fqdn qquery = do+ res <- rawRequest url "resources" (PDB.showQuery qquery)+ case res of+ Left x -> return $ Left x+ Right y -> return $ Right $ filter ( isNotLocal fqdn ) y++pdbRequest :: String -> String -> PDB.Query -> IO (Either String ResolvedValue)+pdbRequest url querytype qquery = rawRequest url querytype (PDB.showQuery qquery)++rawRequest :: (FromJSON a) => String -> String -> String -> IO (Either String a)+rawRequest url querytype query = runErrorT $ do+ unless (querytype `elem` ["resources", "nodes", "facts"]) (throwError $ "Invalid query type " ++ querytype)+ let q = case querytype of+ "facts" -> '/' : query+ _ -> "?" ++ (BC.unpack $ W.renderSimpleQuery False [("query", BC.pack query)])+ fullurl = url ++ "/" ++ querytype ++ q+ initReq <- case (parseUrl fullurl :: Maybe (Request a)) of+ Just x -> return x+ Nothing -> throwError "Something failed when parsing the PuppetDB URL"+ let req = initReq { requestHeaders = [("Accept", "application/json")] }+ runRequest req
language-puppet.cabal view
@@ -7,7 +7,7 @@ -- The package version. See the Haskell package versioning policy -- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for -- standards guiding when and how versions should be incremented.-Version: 0.1.7.2+Version: 0.1.8.0 -- A short (one-line) description of the package. Synopsis: Tools to parse and evaluate the Puppet DSL.@@ -52,11 +52,11 @@ ghc-options: -Wall -fno-warn-missing-signatures -fno-warn-unused-do-bind -funbox-strict-fields -- Modules exported by the library. Exposed-modules: Puppet.DSL.Parser, Puppet.DSL.Printer, Puppet.Daemon, Puppet.Init, Puppet.DSL.Loader, Puppet.Printers, Puppet.NativeTypes, Puppet.DSL.Types,- Puppet.Interpreter.Types, Puppet.Interpreter.Catalog, Puppet.NativeTypes.Helpers, PuppetDB.Rest+ Puppet.Interpreter.Types, Puppet.Interpreter.Catalog, Puppet.NativeTypes.Helpers, PuppetDB.Rest, PuppetDB.Query -- Packages needed in order to build this package. Build-depends: base >=3 && <5,parsec,MissingH,containers,pretty,mtl,unix,hslogger,filepath,Glob,regexpr,process,bytestring,cryptohash,base16-bytestring,regex-pcre-builtin,- iconv, text, unordered-containers, aeson, vector, http-types, http-conduit, attoparsec+ iconv, text, unordered-containers, aeson, vector, http-types, http-conduit, attoparsec, failure -- Modules not exported by this package. Other-modules: Puppet.Interpreter.Functions, Puppet.NativeTypes.File, Erb.Compute, SafeProcess, Paths_language_puppet, Erb.Parser, Erb.Ruby, Erb.Evaluate
ruby/calcerb.rb view
@@ -15,7 +15,7 @@ elsif has_variable?(@context + "::" + name) @mvars[@context + "::" + name] else- :undef+ throw("Unknown variable " + name) end end