packages feed

project-m36 1.0.1 → 1.0.2

raw patch · 5 files changed

+396/−106 lines, 5 filesdep ~curryer-rpcdep ~streamlyPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: curryer-rpc, streamly

API changes (from Hackage documentation)

- ProjectM36.Base: DifferenceF :: r_aFkX -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: DifferenceF :: r_aFEe -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: EqualsF :: r_aFkX -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: EqualsF :: r_aFEe -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: ExistingRelationF :: Relation -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: ExistingRelationF :: Relation -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: ExtendF :: ExtendTupleExprBase a_atbU -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: ExtendF :: ExtendTupleExprBase a_atpY -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: GroupF :: AttributeNamesBase a_atbU -> Text -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: GroupF :: AttributeNamesBase a_atpY -> Text -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: JoinF :: r_aFkX -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: JoinF :: r_aFEe -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: MakeRelationFromExprsF :: Maybe [AttributeExprBase a_atbU] -> TupleExprsBase a_atbU -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: MakeRelationFromExprsF :: Maybe [AttributeExprBase a_atpY] -> TupleExprsBase a_atpY -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: MakeStaticRelationF :: Attributes -> RelationTupleSet -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: MakeStaticRelationF :: Attributes -> RelationTupleSet -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: NotEqualsF :: r_aFkX -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: NotEqualsF :: r_aFEe -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: ProjectF :: AttributeNamesBase a_atbU -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: ProjectF :: AttributeNamesBase a_atpY -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: RelationValuedAttributeF :: Text -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: RelationValuedAttributeF :: Text -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: RelationVariableF :: Text -> a_atbU -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: RelationVariableF :: Text -> a_atpY -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: RenameF :: Set (Text, Text) -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: RenameF :: Set (Text, Text) -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: RestrictF :: RestrictionPredicateExprBase a_atbU -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: RestrictF :: RestrictionPredicateExprBase a_atpY -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: UngroupF :: Text -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: UngroupF :: Text -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: UnionF :: r_aFkX -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: UnionF :: r_aFEe -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: WithF :: [(WithNameExprBase a_atbU, r_aFkX)] -> r_aFkX -> RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: WithF :: [(WithNameExprBase a_atpY, r_aFEe)] -> r_aFEe -> RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.Base: data RelationalExprBaseF (a_atbU :: Type) r_aFkX
+ ProjectM36.Base: data RelationalExprBaseF (a_atpY :: Type) r_aFEe
- ProjectM36.SQL.Select: BetweenOperatorF :: r_a1JfP -> r_a1JfP -> r_a1JfP -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: BetweenOperatorF :: r_a1Jtz -> r_a1Jtz -> r_a1Jtz -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: BinaryOperatorF :: r_a1JfP -> OperatorName -> r_a1JfP -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: BinaryOperatorF :: r_a1Jtz -> OperatorName -> r_a1Jtz -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: BooleanLiteralF :: Bool -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: BooleanLiteralF :: Bool -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: BooleanOperatorExprF :: r_a1JfP -> BoolOp -> r_a1JfP -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: BooleanOperatorExprF :: r_a1Jtz -> BoolOp -> r_a1Jtz -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: CaseExprF :: [(r_a1JfP, r_a1JfP)] -> Maybe r_a1JfP -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: CaseExprF :: [(r_a1Jtz, r_a1Jtz)] -> Maybe r_a1Jtz -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: DoubleLiteralF :: Double -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: DoubleLiteralF :: Double -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: ExistsExprF :: Select -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: ExistsExprF :: Select -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: FunctionApplicationF :: FuncName -> [r_a1JfP] -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: FunctionApplicationF :: FuncName -> [r_a1Jtz] -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: IdentifierF :: n_a1ziD -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: IdentifierF :: n_a1zwA -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: InExprF :: InFlag -> r_a1JfP -> InPredicateValue -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: InExprF :: InFlag -> r_a1Jtz -> InPredicateValue -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: IntegerLiteralF :: Integer -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: IntegerLiteralF :: Integer -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: NullLiteralF :: ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: NullLiteralF :: ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: PostfixOperatorF :: r_a1JfP -> OperatorName -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: PostfixOperatorF :: r_a1Jtz -> OperatorName -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: PrefixOperatorF :: OperatorName -> r_a1JfP -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: PrefixOperatorF :: OperatorName -> r_a1Jtz -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: QuantifiedComparisonF :: r_a1JfP -> ComparisonOperator -> QuantifiedComparisonPredicate -> Select -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: QuantifiedComparisonF :: r_a1Jtz -> ComparisonOperator -> QuantifiedComparisonPredicate -> Select -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: StringLiteralF :: Text -> ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: StringLiteralF :: Text -> ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz
- ProjectM36.SQL.Select: [caseElseF] :: ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP -> Maybe r_a1JfP
+ ProjectM36.SQL.Select: [caseElseF] :: ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz -> Maybe r_a1Jtz
- ProjectM36.SQL.Select: [caseWhensF] :: ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP -> [(r_a1JfP, r_a1JfP)]
+ ProjectM36.SQL.Select: [caseWhensF] :: ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz -> [(r_a1Jtz, r_a1Jtz)]
- ProjectM36.SQL.Select: [qcExprF] :: ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP -> r_a1JfP
+ ProjectM36.SQL.Select: [qcExprF] :: ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz -> r_a1Jtz
- ProjectM36.SQL.Select: [qcOperatorF] :: ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP -> ComparisonOperator
+ ProjectM36.SQL.Select: [qcOperatorF] :: ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz -> ComparisonOperator
- ProjectM36.SQL.Select: [qcPredicateF] :: ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP -> QuantifiedComparisonPredicate
+ ProjectM36.SQL.Select: [qcPredicateF] :: ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz -> QuantifiedComparisonPredicate
- ProjectM36.SQL.Select: [qcQueryF] :: ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP -> Select
+ ProjectM36.SQL.Select: [qcQueryF] :: ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz -> Select
- ProjectM36.SQL.Select: data ScalarExprBaseF (n_a1ziD :: Type) r_a1JfP
+ ProjectM36.SQL.Select: data ScalarExprBaseF (n_a1zwA :: Type) r_a1Jtz

Files

Changelog.markdown view
@@ -1,3 +1,9 @@+# 2024-07-27 (v1.0.2)++* upgrade to streamly 0.10.1+* refactor websocket API to support request UUIDs (#367)+* refactor websocket API to support `createSessionAtHead` (#367)+ # 2024-07-15 (v1.0.1)  * add support for aggregate functions using sub-relation attributes
project-m36.cabal view
@@ -1,6 +1,6 @@ Cabal-Version: 2.2 Name: project-m36-Version: 1.0.1+Version: 1.0.2 License: MIT --note that this license specification is erroneous and only labeled MIT to appease hackage which does not recognize public domain packages in cabal >2.2- Project:M36 is dedicated to the public domain Build-Type: Simple@@ -36,7 +36,67 @@      Default: True  Library-    Build-Depends: base>=4.16 && < 4.19, ghc-paths, mtl, containers, unordered-containers, hashable, haskeline, directory, MonadRandom, random-shuffle, uuid >= 1.3.12, cassava >= 0.4.5.1 && < 0.6, text, bytestring, deepseq, deepseq-generics, vector, parallel, monad-parallel, exceptions, transformers, gnuplot, filepath, zlib, directory, temporary, stm, time, old-locale, rset, attoparsec, either, base64-bytestring, data-interval, extended-reals, aeson >= 1.1, path-pieces, conduit, resourcet, http-api-data, semigroups, QuickCheck, quickcheck-instances, list-t, stm-containers >= 0.2.15, foldl, optparse-applicative, Glob, cryptohash-sha256, text-manipulate >= 0.2.0.1 && < 0.4, winery >= 1.4, curryer-rpc>=0.3.5, network, async, vector-instances, recursion-schemes, streamly == 0.9.0, convertible, fast-builder, scientific+    Build-Depends:+                  base>=4.16 && < 4.19,+                  ghc-paths,+                  mtl,+                  containers,+                  unordered-containers,+                  hashable,+                  haskeline,+                  directory,+                  MonadRandom, +                  random-shuffle,+                  uuid >= 1.3.12,+                  cassava >= 0.4.5.1 && < 0.6,+                  text,+                  bytestring,+                  deepseq,+                  deepseq-generics,+                  vector,+                  parallel,+                  monad-parallel,+                  exceptions,+                  transformers,+                  gnuplot,+                  filepath,+                  zlib,+                  directory,+                  temporary,+                  stm,+                  time,+                  old-locale,+                  rset,+                  attoparsec,+                  either,+                  base64-bytestring,+                  data-interval,+                  extended-reals,+                  aeson >= 1.1,+                  path-pieces,+                  conduit,+                  resourcet,+                  http-api-data,+                  semigroups,+                  QuickCheck,+                  quickcheck-instances,+                  list-t,+                  stm-containers >= 0.2.15,+                  foldl,+                  optparse-applicative,+                  Glob,+                  cryptohash-sha256,+                  text-manipulate >= 0.2.0.1 && < 0.4,+                  winery >= 1.4,+                  curryer-rpc>=0.3.6,+                  network,+                  async,+                  vector-instances,+                  recursion-schemes,+                  streamly == 0.10.1,+                  convertible,+                  fast-builder,+                  scientific     if flag(haskell-scripting)         Build-Depends: ghc >= 9.0 && < 9.5         CPP-Options: -DPM36_HASKELL_SCRIPTING@@ -566,7 +626,7 @@     import: commontest     type: exitcode-stdio-1.0     main-is: Server/WebSocket.hs-    Other-Modules: TutorialD.Interpreter.Export.Base, TutorialD.Interpreter.Export.CSV, TutorialD.Interpreter.Import.BasicExamples, TutorialD.Interpreter.Import.CSV, TutorialD.Interpreter.InformationOperator, TutorialD.Interpreter.RODatabaseContextOperator, TutorialD.Interpreter.TransactionGraphOperator, ProjectM36.Client.Json, ProjectM36.Server.RemoteCallTypes.Json, ProjectM36.Server.WebSocket, TutorialD.Interpreter, TutorialD.Interpreter.Base, TutorialD.Interpreter.DatabaseContextExpr, TutorialD.Interpreter.Import.Base, TutorialD.Interpreter.Import.TutorialD, TutorialD.Interpreter.RelationalExpr, TutorialD.Interpreter.Types, TutorialD.Interpreter.DatabaseContextIOOperator, TutorialD.Interpreter.TransGraphRelationalOperator, TutorialD.Interpreter.SchemaOperator, TutorialD.Printer+    Other-Modules: TutorialD.Interpreter.Export.Base, TutorialD.Interpreter.Export.CSV, TutorialD.Interpreter.Import.BasicExamples, TutorialD.Interpreter.Import.CSV, TutorialD.Interpreter.InformationOperator, TutorialD.Interpreter.RODatabaseContextOperator, TutorialD.Interpreter.TransactionGraphOperator, ProjectM36.Client.Json, ProjectM36.Server.RemoteCallTypes.Json, ProjectM36.Server.WebSocket, TutorialD.Interpreter, TutorialD.Interpreter.Base, TutorialD.Interpreter.DatabaseContextExpr, TutorialD.Interpreter.Import.Base, TutorialD.Interpreter.Import.TutorialD, TutorialD.Interpreter.RelationalExpr, TutorialD.Interpreter.Types, TutorialD.Interpreter.DatabaseContextIOOperator, TutorialD.Interpreter.TransGraphRelationalOperator, TutorialD.Interpreter.SchemaOperator, TutorialD.Printer, ProjectM36.Interpreter  Test-Suite test-isomorphic-schemas     import: commontest
src/bin/ProjectM36/Server/RemoteCallTypes/Json.hs view
@@ -110,6 +110,11 @@                                       "val" .= u ]   toJSON atom@(RelationAtom i) = object [ "type" .= atomTypeForAtom atom,                                           "val" .= i ]+  toJSON atom@(SubrelationFoldAtom rel attrName) = object [ "type" .= atomTypeForAtom atom,+                                                            "val" .=+                                                            object [ "relation" .= rel,+                                                                     "attributeName" .= attrName]+                                                          ]   toJSON atom@(RelationalExprAtom i) = object [ "type" .= atomTypeForAtom atom,                                             "val" .= i ]   toJSON (ConstructedAtom dConsName atomtype atomlist) = object [@@ -148,6 +153,9 @@           Just u -> pure $ UUIDAtom u           Nothing -> fail "Invalid UUID String"       RelationalExprAtomType -> RelationalExprAtom <$> o .: "val"+      SubrelationFoldAtomType _ -> do+        val <- o .: "val"+        SubrelationFoldAtom <$> val .: "relation" <*> val .: "attributeName"  instance ToJSON Notification instance FromJSON Notification
src/bin/ProjectM36/Server/WebSocket.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE LambdaCase, CPP #-}+{-# LANGUAGE LambdaCase, CPP, GeneralizedNewtypeDeriving, DeriveAnyClass, DeriveGeneric, DerivingStrategies, ScopedTypeVariables #-} module ProjectM36.Server.WebSocket where -- while the tutd client performs TutorialD parsing on the client, the websocket server will pass tutd to be parsed and executed on the server- otherwise I have to pull in ghcjs as a dependency to allow client-side parsing- that's not appealing because then the frontend is not language-agnostic, but this could change in the future, perhaps by sending different messages over the websocket -- ideally, the wire protocol should not be exposed to a straight string-based API ala SQL, so we could make perhaps a javascript DSL which compiles to the necessary JSON- anaylyze tradeoffs@@ -14,15 +14,17 @@ import ProjectM36.Relation.Show.Term import ProjectM36.Relation.Show.HTML import Data.Aeson+import ProjectM36.Base import TutorialD.Interpreter import ProjectM36.Interpreter (ConsoleResult(..), SafeEvaluationFlag(..)) import ProjectM36.Client import Control.Exception-import Data.Attoparsec.Text-import Control.Applicative import Text.Megaparsec.Error-import Data.Functor import Data.Either (fromRight)+import qualified Data.ByteString.Lazy as BS+import qualified Data.UUID as UUID+import GHC.Generics+import qualified Data.Text.Encoding as TE  #if MIN_VERSION_megaparsec(7,0,0) import Data.List.NonEmpty as NE@@ -31,94 +33,119 @@ websocketProxyServer :: Port -> Hostname -> WS.ServerApp websocketProxyServer port host pending = do   conn <- WS.acceptRequest pending-  let unexpectedMsg = WS.sendTextData conn ("messagenotexpected" :: T.Text)+  let unexpectedMsg expecting received = do+        print (expecting, received)+        sendResponse conn (MessageNotExpected expecting)   --phase 1- accept database name for connection-  dbmsg <- WS.receiveData conn :: IO T.Text-  let connectdbmsg = "connectdb:"-  if not (connectdbmsg `T.isPrefixOf` dbmsg) then unexpectedMsg >> WS.sendClose conn ("" :: T.Text)-    else do-        let dbname = T.unpack $ T.drop (T.length connectdbmsg) dbmsg+  dbmsg <- WS.receiveData conn :: IO BS.ByteString+  case eitherDecode dbmsg of+    Left _ -> do+      unexpectedMsg "ConnectionSetupRequest" dbmsg+      WS.sendClose conn ("" :: T.Text)+    Right (ConnectionSetupRequest dbname) -> do         bracket (createConnection conn dbname port host)           (\case               Right dbconn -> close dbconn               Left _ -> pure ()) $ \case-            Left err -> sendError conn err+            Left err -> sendResponse conn (TextErrorResponse (RequestId UUID.nil) (T.pack (show err)))             Right dbconn -> do-                eSessionId <- createSessionAtHead dbconn "master"-                case eSessionId of-                  Left err -> sendError conn err-                  Right sessionId -> do-                    --phase 2- accept tutoriald commands-                    _ <- forever $ do-                      pInfo <- promptInfo sessionId dbconn-                      --figure out why sending three times during startup is necessary-                      sendPromptInfo pInfo conn-                      sendPromptInfo pInfo conn-                      msg <- WS.receiveData conn :: IO T.Text-                      case parseOnly parseExecuteMessage msg of-                        Left _ -> unexpectedMsg-                        Right (presentation, tutdString) ->+              sendResponse conn ConnectionSetupResponse+              --phase 2- accept tutoriald commands+              _ <- forever $ do+                {-pInfo <- promptInfo sessionId dbconn+                --figure out why sending three times during startup is necessary+                sendPromptInfo pInfo conn+                sendPromptInfo pInfo conn-}+                msg <- WS.receiveData conn :: IO BS.ByteString+                case eitherDecode msg of+                        Left err -> unexpectedMsg "Request" err+                        Right (CreateSessionAtHeadRequest reqId branchName) -> do+                          ret <- createSessionAtHead dbconn branchName+                          case ret of+                            Left err -> sendResponse conn (RelationalErrorResponse reqId err)+                            Right sessionId -> sendResponse conn (CreateSessionAtHeadResponse reqId sessionId) +                        Right (ExecuteTutorialDRequest reqId sessionId presentation (TutorialDText tutdString)) ->                           case parseTutorialD tutdString of-                            Left err -> handleOpResult conn dbconn presentation-#if MIN_VERSION_megaparsec(7,0,0)-                              (DisplayErrorResult-                                ("parse error: " `T.append` T.pack-                                  (parseErrorPretty . NE.head . bundleErrors $ err)))-#else-                              (DisplayErrorResult ("parse error: " `T.append` T.pack (parseErrorPretty err)))-#endif+                            Left err -> do+                              let parseErr = ParseError $ T.pack+                                    (parseErrorPretty . NE.head . bundleErrors $ err)++                              sendResponse conn (RelationalErrorResponse reqId parseErr)                             Right parsed -> do                               let timeoutFilter exc = if exc == RequestTimeoutException                                                           then Just exc                                                           else Nothing                                   responseHandler = do                                     result <- evalTutorialD sessionId dbconn SafeEvaluation parsed-                                    pInfo' <- promptInfo sessionId dbconn-                                    sendPromptInfo pInfo' conn-                                    handleOpResult conn dbconn presentation result-                              catchJust timeoutFilter responseHandler (\_ -> handleOpResult conn dbconn presentation (DisplayErrorResult "Request Timed Out."))-                    pure ()+                                    (headName', schemaName) <- promptInfo sessionId dbconn+                                    sendResponse conn (PromptInfoResponse sessionId headName' schemaName)+                                    let resp = makeResponse reqId presentation result+                                    handleResponse conn dbconn resp+                              catchJust timeoutFilter responseHandler (\_ ->+                                                                         sendResponse conn (TimeoutResponse reqId))+              pure () +         notificationCallback :: WS.Connection -> NotificationCallback-notificationCallback conn notifName evaldNotif = WS.sendTextData conn (encode (object ["notificationname" .= notifName,-                                                                                       "evaldnotification" .= evaldNotif-                                        ]))+notificationCallback conn notifName evaldNotif =+  sendResponse conn (NotificationResponse notifName evaldNotif)  --this creates a new database for each connection- perhaps not what we want (?) createConnection :: WS.Connection -> DatabaseName -> Port -> Hostname -> IO (Either ConnectionError Connection) createConnection wsconn dbname port host = connectProjectM36 (RemoteConnectionInfo dbname host (show port) (notificationCallback wsconn)) -sendError :: (ToJSON a) => WS.Connection -> a -> IO ()-sendError conn err = WS.sendTextData conn (encode (object ["displayerror" .= err]))--handleOpResult :: WS.Connection -> Connection -> Presentation -> ConsoleResult -> IO ()-handleOpResult conn db _ QuitResult = WS.sendClose conn ("close" :: T.Text) >> close db-handleOpResult conn  _ _ (DisplayResult out) = WS.sendTextData conn (encode (object ["display" .= out]))-handleOpResult _ _ _ (DisplayIOResult ioout) = ioout-handleOpResult conn _ presentation (DisplayErrorResult err) = do-  let jsono = ["json" .= err | jsonPresentation presentation]-      texto = ["text" .= err | textPresentation presentation]-      htmlo = ["html" .= err | htmlPresentation presentation]-  WS.sendTextData conn (encode (object ["displayerror" .= object (jsono ++ texto ++ htmlo)]))-handleOpResult conn _ _ (DisplayParseErrorResult _ err) = WS.sendTextData conn (encode (object ["displayparseerrorresult" .= show err]))-handleOpResult conn _ _ QuietSuccessResult = WS.sendTextData conn (encode (object ["acknowledged" .= True]))-handleOpResult conn _ presentation (DisplayRelationResult rel) = do-  let jsono = ["json" .= rel | jsonPresentation presentation]-      texto = ["text" .= showRelation rel | textPresentation presentation]-      htmlo = ["html" .= relationAsHTML rel | htmlPresentation presentation]-  WS.sendTextData conn (encode (object ["displayrelation" .= object (jsono ++ texto ++ htmlo)]))-handleOpResult conn _ presentation (DisplayDataFrameResult df) = do-  let jsono = ["json" .= df | jsonPresentation presentation]-      texto = ["text" .= showDataFrame df | textPresentation presentation]-      htmlo = ["html" .= dataFrameAsHTML df | htmlPresentation presentation]-  WS.sendTextData conn (encode (object ["displaydataframe" .= object (jsono ++ texto ++ htmlo)]))-handleOpResult conn _ _ (DisplayRelationalErrorResult relErr) =-  WS.sendTextData conn (encode (object ["displayrelationalerrorresult" .= relErr]))-handleOpResult conn dbconn presentation (DisplayHintWith txt conResult) = do-  -- we should wrap this up into one response instead of two responses for clarity-  WS.sendTextData conn (encode (object ["hint" .= txt]))-  handleOpResult conn dbconn presentation conResult-+handleResponse :: WS.Connection -> Connection -> Response -> IO ()+handleResponse conn dbconn resp =+  case resp of+    ConnectionClosedResponse -> do+      close dbconn +      sendResp+    DisplayTextResponse{} -> sendResp+    RelationResponse{} -> sendResp+    DataFrameResponse{} -> sendResp+    CreateSessionAtHeadResponse{} -> sendResp+    SuccessResponse{} -> sendResp+    TimeoutResponse{} -> sendResp+    PromptInfoResponse{} -> sendResp+    RelationalErrorResponse{} -> sendResp+    TextErrorResponse{} -> sendResp+    HintWithResponse{} -> sendResp+    ConnectionSetupResponse{} -> sendResp+    NotificationResponse{} -> sendResp+    MessageNotExpected{} -> sendResp+    where+      sendResp = sendResponse conn resp+    +makeResponse :: RequestId -> Presentation -> ConsoleResult -> Response+makeResponse reqId presentation consoleResult =+  case consoleResult of+    QuitResult -> do+      ConnectionClosedResponse+    DisplayResult out ->+      DisplayTextResponse reqId out+    DisplayIOResult _ ->+      -- we can't send it over the websocket, so just ignore it- in other context, this just used to launch plots       +      SuccessResponse reqId+    DisplayErrorResult err ->+      TextErrorResponse reqId err+    DisplayParseErrorResult _ err -> +      let err' = ParseError $ T.pack (parseErrorPretty . NE.head . bundleErrors $ err) in+        RelationalErrorResponse reqId err'+    QuietSuccessResult ->+      SuccessResponse reqId+    DisplayRelationResult rel ->+      RelationResponse reqId rel presentation+    DisplayDataFrameResult df -> +      DataFrameResponse reqId df presentation+    DisplayRelationalErrorResult relErr ->+      RelationalErrorResponse reqId relErr+    DisplayHintWith txt conResult ->+      HintWithResponse reqId txt (makeResponse reqId presentation conResult)+  +sendResponse :: WS.Connection -> Response -> IO ()+sendResponse conn response =+  -- this could be optimized to work with a lazy bytestring+  WS.sendTextData conn (TE.decodeUtf8 (BS.toStrict (encode response)))  -- get current schema and head name for client promptInfo :: SessionId -> Connection -> IO (HeadName, SchemaName)@@ -135,19 +162,189 @@   jsonPresentation :: Bool,   textPresentation :: Bool,   htmlPresentation :: Bool }+  deriving (Generic, ToJSON, FromJSON) ++jsonOnlyPresentation :: Presentation+jsonOnlyPresentation = Presentation { jsonPresentation = True,+                                  textPresentation = False,+                                  htmlPresentation = False }+                    data PresentationFlag = JSONFlag | TextFlag | HTMLFlag+  deriving (Generic, ToJSON, FromJSON) -parseExecuteMessage :: Parser (Presentation, T.Text)-parseExecuteMessage = do-  _ <- string "executetutd/"-  flags <- sepBy ((string "json" $> JSONFlag) <|>-                  (string "text" $> TextFlag) <|>-                  (string "html" $> HTMLFlag)) "+"-  let presentation = foldr (\flag acc -> case flag of-                               JSONFlag -> acc {jsonPresentation = True}-                               TextFlag -> acc {textPresentation = True}-                               HTMLFlag -> acc {htmlPresentation = True}) (Presentation False False False) flags-  _ <- char ':'-  tutd <- T.pack <$> manyTill anyChar endOfInput-  pure (presentation, tutd)+newtype RequestId = RequestId UUID.UUID+  deriving newtype (ToJSON, FromJSON, Eq, Show)++newtype TutorialDText = TutorialDText T.Text+  deriving newtype (ToJSON, FromJSON)++newtype ConnectionSetupRequest = ConnectionSetupRequest DatabaseName++instance FromJSON ConnectionSetupRequest where+  parseJSON = withObject "ConnectionSetupRequest" $ \o -> do+    tag::T.Text <- o .: "tag"+    case tag of+      "ConnectionSetupRequest" ->+        ConnectionSetupRequest <$> o .: "databaseName"+      other -> fail ("bad tag: " <> show other)++instance ToJSON ConnectionSetupRequest where+  toJSON (ConnectionSetupRequest dbName) =+    object [ "tag" .= ("ConnectionSetupRequest"::T.Text),+             "databaseName" .= dbName ]++data Request = ExecuteTutorialDRequest RequestId SessionId Presentation TutorialDText |+               CreateSessionAtHeadRequest RequestId HeadName++data Response = RelationResponse RequestId Relation Presentation |+                DataFrameResponse RequestId DataFrame Presentation |+                CreateSessionAtHeadResponse RequestId SessionId |+                SuccessResponse RequestId |+                TimeoutResponse RequestId |+                PromptInfoResponse SessionId HeadName SchemaName |+                RelationalErrorResponse RequestId RelationalError |+                TextErrorResponse RequestId T.Text |+                ConnectionClosedResponse |+                ConnectionSetupResponse |+                DisplayTextResponse RequestId T.Text |+                HintWithResponse RequestId T.Text Response |+                NotificationResponse NotificationName EvaluatedNotification |+                MessageNotExpected T.Text++instance ToJSON Request where+  toJSON (ExecuteTutorialDRequest reqId sessionId presentation tutd) =+    object [ "tag" .= ("ExecuteTutorialDRequest"::T.Text),+             "requestId" .= reqId,+             "sessionId" .= sessionId,+             "presentation" .= presentation,+             "tutoriald" .= tutd ]+  toJSON (CreateSessionAtHeadRequest reqId headName') =+    object [ "tag" .= ("CreateSessionAtHeadRequest"::T.Text),+             "requestId" .= reqId,+             "headName" .= headName' ]++instance FromJSON Request where+  parseJSON = withObject "ExecuteTutorialDRequest" $ \o -> do+    tag::T.Text <- o .: "tag"+    case tag of+      "ExecuteTutorialDRequest" ->+        ExecuteTutorialDRequest <$> o .: "requestId"+                                <*> o .: "sessionId"+                                <*> o .: "presentation"+                                <*> o .: "tutoriald"+      "CreateSessionAtHeadRequest" ->+        CreateSessionAtHeadRequest <$> o .: "requestId"+                                   <*> o .: "headName"+      other -> fail ("bad tag: " <> show other)++-- this is useful for Haskell clients to decode JSON messages+instance FromJSON Response where+  parseJSON = withObject "Response" $ \o -> do+    tag::T.Text <- o .: "tag"+    case tag of+      "RelationResponse" -> do+        mJsonRel <- o .: "jsonRelation"+        case mJsonRel of+          Nothing -> fail "missing json relation"+          Just jsonRel ->+            RelationResponse <$>  o .: "requestId" <*> pure jsonRel <*> pure jsonOnlyPresentation+      "DataFrameResponse" -> do+        mJsonDF <- o .: "jsonDataFrame"+        case mJsonDF of+          Nothing -> fail "missing json dataframe"+          Just jsonDF ->+            DataFrameResponse <$> o .: "requestId" <*> pure jsonDF <*> pure jsonOnlyPresentation+      "CreateSessionAtHeadResponse" ->+        CreateSessionAtHeadResponse <$> o .: "requestId" <*> o .: "sessionId"+      "SuccessResponse" ->+        SuccessResponse <$> o .: "requestId"+      "TimeoutResponse" ->+        TimeoutResponse <$> o .: "requestId"+      "PromptInfoResponse" ->+        PromptInfoResponse <$> o .: "sessionId" <*> o .: "headName" <*> o .: "schemaName"+      "RelationalErrorResponse" ->+        RelationalErrorResponse <$> o .: "requestId" <*> o .: "error"+      "TextErrorResponse" ->+        TextErrorResponse <$> o .: "requestId" <*> o .: "error"+      "ConnectionClosedResponse" ->+        pure ConnectionClosedResponse+      "DisplayTextResponse" ->+        DisplayTextResponse <$> o .: "requestId" <*> o .: "text"+      "HintWithResponse" ->+        HintWithResponse <$> o .: "requestId" <*> o .: "hintText" <*> o .: "response"+      "ConnectionSetupResponse" ->+        pure ConnectionSetupResponse+      "NotificationResponse" ->+        NotificationResponse <$> o .: "notificationName" <*> o .: "evaluatedNotification"+      "MessageNotExpected" ->+        MessageNotExpected <$> o .: "expected"+      other ->+        fail ("unexpected tag: " <> T.unpack other)++instance ToJSON Response where+  toJSON (RelationResponse reqId rel presentation) =+    object [ "tag" .= ("RelationResponse"::T.Text),+             "requestId" .= reqId,+             "jsonRelation" .= if jsonPresentation presentation then+               Just rel else Nothing,+             "textRelation" .= if textPresentation presentation then+               Just (showRelation rel) else Nothing,+             "htmlRelation" .= if htmlPresentation presentation then+               Just (relationAsHTML rel) else Nothing+             +           ]+  toJSON (DataFrameResponse reqId df presentation) =+    object [ "tag" .= ("DataFrameResponse"::T.Text),+             "requestId" .= reqId,+             "jsonDataFrame" .= if jsonPresentation presentation then+               Just df else Nothing,+             "textDataFrame" .= if textPresentation presentation then+               Just (showDataFrame df) else Nothing,+             "htmlDataFrame" .= if htmlPresentation presentation then+               Just (dataFrameAsHTML df) else Nothing+             ]+  toJSON (CreateSessionAtHeadResponse reqId sessionId) =+    object [ "tag" .= ("CreateSessionAtHeadResponse"::T.Text),+             "requestId" .= reqId,+             "sessionId" .= sessionId ]+  toJSON (SuccessResponse reqId) =+    object [ "tag" .= ("SuccessResponse"::T.Text),+             "requestId" .= reqId ]+  toJSON (TimeoutResponse reqId) = +    object [ "tag" .= ("TimeoutResponse"::T.Text),+             "requestId" .= reqId ]+  toJSON (PromptInfoResponse sessionId headName' schemaName) =+    object [ "tag" .= ("PromptInfoResponse"::T.Text),+             "sessionId" .= sessionId,+             "headName" .= headName',+             "schemaName" .= schemaName ]+  toJSON (RelationalErrorResponse reqId relErr) =+    object [ "tag" .= ("RelationalErrorResponse"::T.Text),+             "requestId" .= reqId,+             "error" .= relErr ]+  toJSON (TextErrorResponse reqId err) =+    object [ "tag" .= ("TextErrorResponse"::T.Text),+             "requestId" .= reqId,+             "error" .= err ]+  toJSON ConnectionClosedResponse =+    object [ "tag" .= ("ConnectionClosedResponse"::T.Text) ]+  toJSON (DisplayTextResponse reqId txt) =+    object [ "tag" .= ("DisplayTextResponse"::T.Text),+             "requestId" .= reqId,+             "text" .= txt ]+  toJSON (HintWithResponse reqId hintTxt resp) =+    object [ "tag" .= ("HintWithResponse"::T.Text),+             "requestId" .= reqId,+             "hintText" .= hintTxt,+             "response" .= resp ]+  toJSON ConnectionSetupResponse =+    object [ "tag" .= ("ConnectionSetupResponse"::T.Text),+             "status" .= ("ready"::T.Text) ]+  toJSON (NotificationResponse notificationName evaldNotification) =+    object [ "tag" .= ("NotificationResponse"::T.Text),+             "notificationName" .= notificationName,+             "evaluatedNotification" .= evaldNotification ]+  toJSON (MessageNotExpected expected) =+    object [ "tag" .= ("MessageNotExpected"::T.Text),+             "expected" .= expected ]
test/Server/WebSocket.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP, TypeApplications #-} -- test the websocket server import Test.HUnit import qualified Network.WebSockets as WS@@ -6,7 +6,6 @@ import ProjectM36.Server.Config import ProjectM36.Server import ProjectM36.Client-import ProjectM36.Base  import Network.Socket import Control.Exception@@ -15,10 +14,10 @@ import Data.Typeable import Data.Text hiding (map) import Data.Aeson-import qualified Data.Map as M import qualified Data.ByteString.Lazy as BS import ProjectM36.Relation import Control.Monad (void)+import Data.UUID.V4 (nextRandom)  --start the websocket server -- run some tutoriald against it@@ -75,35 +74,55 @@ basicConnection port = WS.runClient "127.0.0.1" (fromIntegral port) "/"  basicConnectionWithDatabase :: PortNumber -> DatabaseName -> WS.ClientApp () -> IO ()-basicConnectionWithDatabase port dbname block = basicConnection port (\conn -> do-                                                                WS.sendTextData conn ("connectdb:" `append` pack dbname)-                                                                block conn)+basicConnectionWithDatabase port dbname block = basicConnection port+  (\conn -> do+      --setup connection+      let connectMsg = encode (ConnectionSetupRequest dbname)+      WS.sendTextData conn connectMsg+      setupResponse <- WS.receiveData conn+      case eitherDecode setupResponse of+        Left err -> error (show err)+        Right ConnectionSetupResponse{} -> pure ()+        Right _other -> error (show setupResponse)+      block conn)      testBasicConnection :: PortNumber -> DatabaseName -> Test-testBasicConnection port _ = TestCase $ basicConnection port (\conn -> WS.sendClose conn ("test close"::Text))+testBasicConnection port _ = TestCase $ basicConnection port (\conn -> WS.sendClose conn (""::Text))  testTutorialD :: PortNumber -> DatabaseName -> Test testTutorialD port dbname = TestCase $ basicConnectionWithDatabase port dbname testtutd   where     discardPromptInfo conn = do       response <- WS.receiveData conn :: IO BS.ByteString-      let decoded = decode response :: Maybe (M.Map Text (M.Map Text Text))-      case decoded of-        Just _ -> pure ()-        Nothing ->  assertFailure ("failed to decode prompt info: " ++ show response)+      case eitherDecode @Response response of+        Right _promptInfo -> pure ()+        Left err -> assertFailure ("failed to decode prompt info: " ++ err ++ ": " ++ show response)            testtutd conn = do-      discardPromptInfo conn-      WS.sendTextData conn ("executetutd/json:" `append` ":showexpr true")-      discardPromptInfo conn+      -- create new session at master+      reqId <- RequestId <$> nextRandom+      let createSessionMsg = encode (CreateSessionAtHeadRequest reqId "master")+      WS.sendTextData conn createSessionMsg+      sessionResponse <- WS.receiveData conn :: IO BS.ByteString+      sessionId <- case eitherDecode sessionResponse of+                     Left err -> assertFailure err+                     Right (CreateSessionAtHeadResponse reqId' sid) -> do+                       assertEqual "request ID round-trip" reqId' reqId+                       pure sid+                     Right _other -> assertFailure ("expected session response but got: " <> show sessionResponse)+      -- send tutd message+      reqIdB <- RequestId <$> nextRandom+      let tutdMsg = encode (ExecuteTutorialDRequest reqIdB sessionId jsonOnlyPresentation (TutorialDText ":showexpr true"))+      WS.sendTextData conn tutdMsg       discardPromptInfo conn              --receive relation response       response <- WS.receiveData conn :: IO BS.ByteString      -      let decoded = decode response :: Maybe (M.Map Text (M.Map Text Relation))+      let decoded = decode @Response response        case decoded of          Nothing -> assertFailure "failed to decode"-        Just decoded' -> do-            assertEqual "round-trip true relation" ((decoded' M.! "displayrelation") M.! "json") relationTrue -            WS.sendClose conn ("test close" :: Text)+        Just (RelationResponse _ trueRel _) -> do+            assertEqual "round-trip true relation" trueRel relationTrue +            WS.sendClose conn ("" :: Text)+        Just _other -> assertFailure ("unexpected response: " <> show response)