{-# LANGUAGE RecordWildCards, PatternSynonyms #-} module YQL ( runYQL ) where import Control.Lens import Control.Monad.Catch (throwM) import Control.Monad.Trans.State.Lazy (evalStateT) import Control.Applicative ((<$>)) import Control.Monad.Trans.Except (catchE) import qualified Data.Aeson as Aeson import Data.Default (def) import Data.Foldable (foldlM, foldrM) import qualified Data.HashMap.Strict as HashMap (empty, insert) import Data.Map (Map) import qualified Data.Map as Map (foldlWithKey, toList) import Data.Monoid ((<>)) import Data.String (fromString) import qualified Data.Text as Text (pack) import qualified Data.Text.Lazy as Text (toStrict) import Data.Text.Lazy.Encoding import qualified Data.Vector as Vector (cons, empty) import Network.HTTP.Conduit (parseUrl) import Network.HTTP.Client.Conduit (newManager) import Network.URI.Template import System.IO (stderr, Handle) import System.Log.Formatter import System.Log.Handler (setFormatter) import System.Log.Handler.Simple import System.Log.Logger import Data.OpenDataTable import Data.YQL import Data.YQL.Response import Data.YQL.Rest import Data.YQL.Result import qualified Language.JavaScript as JS import qualified Language.JavaScript.Host.YQL as JS import qualified Language.JavaScript.Host.Console as JS import qualified Language.JavaScript.Interpret as JS import qualified Language.JavaScript.Parser as JS import YQL.Rest import YQL.Y withFormatter :: GenericHandler Handle -> GenericHandler Handle withFormatter handler = setFormatter handler formatter where formatter = simpleLogFormatter "[$time $loggername $prio] $msg" runYQL :: OpenDataTable -> Map String JS.Primitive -> IO Aeson.Value runYQL ot@(OpenDataTable {..}) vs = do let logName = rootLoggerName stderrHandler <- withFormatter <$> streamHandler stderr DEBUG updateGlobalLogger logName (setLevel DEBUG) updateGlobalLogger logName (setHandlers [stderrHandler]) let selects = [s | SelectBinding s <- openDataTableBindings] case selects of (s@(Select{..}):_) -> do manager <- newManager let env = Map.foldlWithKey (\e k v -> addToEnv k (JS.primitiveToString v) e) newEnv vs request <- maybe (return def) (parseUrl . expand env) selectUrl let _rest = Rest { restHttpManager = manager, restHttpRequest = request } _y = undefined _yql = YQL { yqlRest = _rest, yqlY = _y } mSource = openDataTableExecute <> selectExecute flip evalStateT _yql $ do case mSource of Just source -> do let eAST = JS.parseJavaScript source case eAST of Right ast -> do let hostInit = JS.console >> JS.yql ot s vs eRes <- JS.runJavaScriptT JS.initialState $ do responseObj <- hostInit (JS.interpret ast :: JS.JavaScriptT YQLM JS.Completion) `catchE` (\ v -> do s <- JS.toString v throwM $ YQLExceptionJSRuntimeError s) oi <- use $ JS.internalObject responseObj case oi ^. JS.internalProperty "object" of Just (JS.PropertyData JS.DataDescriptor {..}) -> valueToJSON dataDescriptorValue _ -> return Aeson.Null case eRes of Right res -> return res Left _ -> do throwM YQLExceptionInternalError Left e -> throwM $ YQLExceptionJSParseError e _ -> do Result {..} <- get case resultResponse of ResponseByteString s -> do return $ Aeson.String (Text.toStrict . decodeUtf8 $ s) ResponseJSON j -> return j _ -> throwM YQLExceptionMissingSelect valueToJSON :: (Functor m, Monad m) => JS.Value -> JS.JavaScriptT m Aeson.Value valueToJSON (JS.ValueNull _) = return Aeson.Null valueToJSON (JS.ValueUndefined _) = return Aeson.Null valueToJSON (JS.ValueNumber n) = return $ Aeson.Number (fromRational . toRational $ n) valueToJSON (JS.ValueString s) = return $ Aeson.String (fromString s) valueToJSON (JS.ValueBool b) = return $ Aeson.Bool b valueToJSON (JS.ValueObject o) = do c <- use $ JS.class' o case c of "Object" -> do ps <- use $ JS.properties o jo <- foldlM addObjectField (HashMap.empty) (Map.toList ps) return $ Aeson.Object jo "Array" -> do ps <- use $ JS.properties o ja <- foldrM addArrayItem (Vector.empty) (Map.toList ps) return $ Aeson.Array ja where addObjectField :: (Functor m, Monad m) => Aeson.Object -> (String, JS.Property) -> JS.JavaScriptT m Aeson.Object addObjectField jo (name, property) = case property of JS.PropertyData (JS.DataDescriptor {..}) -> do if dataDescriptorEnumerable then do j <- valueToJSON dataDescriptorValue return $ HashMap.insert (Text.pack name) j jo else return jo _ -> return jo addArrayItem :: (Functor m, Monad m) => (String, JS.Property) -> Aeson.Array -> JS.JavaScriptT m Aeson.Array addArrayItem (name, property) ja = do if name /= "length" then do case property of JS.PropertyData (JS.DataDescriptor {..}) -> do if dataDescriptorEnumerable then do j <- valueToJSON dataDescriptorValue return $ Vector.cons j ja else return ja _ -> return ja else return ja