packages feed

penrose-0.1.1.0: src/Plugins.hs

{-# OPTIONS_HADDOCK prune #-}

module Plugins where

import           Control.Exception          (ErrorCall, try)
import           Data.Aeson                 (decode)
import qualified Data.ByteString.Lazy.Char8 as B
import qualified Data.Map.Strict            as M
import           Env
import           Style
import           Substance
import           SubstanceJSON
import           System.Directory           (getCurrentDirectory,
                                             setCurrentDirectory)
import           System.IO.Unsafe           (unsafePerformIO)
import           System.Process             (callCommand)
import           Utils

--------------------------------------------------------------------------------
-- | 'runPlugin' parses plugin statements from the style program and executes
-- the plugin if one is found. It returns 'Nothing' of there is no plugin found,
-- returns an error if more than one is found, or returns a new Substance program
-- and some Style values if the plugin runs successfully.
runPlugin ::
     SubOut
  -> String
  -> VarEnv
  -> Either CompilerError (Maybe (String, [StyVal]))
runPlugin subOut stySrc elementEnv
    -- Find Substance instantiator plugin (if it exists in Style file + directory)
 = do
  instantiations <- parsePlugins "" stySrc elementEnv
    -- If 1 instantiation, run the plugin, append the resulting Substance program, re-check the full program, and use it in the Style compiler.
    -- If >1 instantiation, throw an error.
  case instantiations of
    [] -> Right $ Nothing
    [pluginName] ->
      let res = unsafePerformIO $ try (instantiateSub pluginName subOut)
      in case res of
           Right (subPlugin, styVals) -> Right $ Just $ (subPlugin, styVals)
           Left err -> Left $ PluginRun $ show (err :: ErrorCall)
    _ ->
      Left $ PluginParse $ "Multiple plugins found in Style; only one allowed."

-- If no instantiations, proceed with Style compiler.
--   putStrLn $ "instantiations found: " ++ (show instantiations)
--------------------------------------------------------------------------------
-- | 'pluginDict' stores the mappings from plugin names to commands to run
-- First entry: plugin name
-- Second entry: plugin directory (relative path OK), no trailing "/"
-- Third entry: command to run, relative to plugin directory
-- TODO: the standard should probably include `configure` and `run`, not just `run`
pluginDict :: M.Map String (String, String)
pluginDict =
  M.fromList
    [ ("haskell-test", ("plugins/haskell-instantiator", "./Main"))
    , ("ddgjs", ("plugins/mesh-plugin", "node mesh-plugin.js"))
    , ("alloy", ("plugins/alloy", "java -cp \"*:.\" AlloyPlugin"))
    , ("raytracing", ("TODO: path", "TODO: command"))
    ]

--------------------------------------------------------------------------------
-- Substance instantiation / plugin calls
-- Don't forget to recompile the plugin!
type SubstanceRaw = String

-- TODO: add more error checking to deal with paths or files that don't exist
-- TODO: this functions requires "values.json", which is not outputed by some plugins
instantiateSub :: String -> SubOut -> IO (SubstanceRaw, [StyVal])
instantiateSub pluginName parsedSub = do
  originalDir <- getCurrentDirectory
  let (dirPath, pluginCmd) =
        catchPathError pluginName (M.lookup pluginName pluginDict)
  putStrLn $ "plugin directory: " ++ dirPath
  putStrLn $ "plugin command: " ++ pluginCmd
    -- NOTE: we are not expecting multiple processes to use these tempfiles
  let outFile = dirPath ++ "/Sub_enduser.json"
  let subInFile = dirPath ++ "/Sub_instantiated.sub"
  let styInFile = dirPath ++ "/values.json"
  writeSubstanceToJSON outFile parsedSub
  setCurrentDirectory dirPath -- Change to plugin dir so the plugin gets the right path. Otherwise pwd sees "penrose/src"
  callCommand pluginCmd
  setCurrentDirectory originalDir -- Return to original directory
  newSubProg <- readFile subInFile
  styVals <- readFile styInFile
  styVals' <- B.readFile styInFile
  putStrLn "Penrose received Sub file: "
  putStrLn newSubProg
  putStrLn "---------------------------"
  putStrLn "Penrose received Sty file: "
  putStrLn styVals
  let styRes = (decode styVals') :: Maybe [StyVal]
  let styJSON =
        case styRes of
          Nothing -> error "couldn't read plugin JSON"
          Just x  -> x
  return (newSubProg, styJSON)

catchPathError :: String -> Maybe (FilePath, String) -> (FilePath, String)
catchPathError name Nothing =
  error $ "path to plugin '" ++ name ++ "' doesn't exist!"
catchPathError _ (Just x) = x