Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify and openapi-ify /api/program #180

Merged
4 commits merged into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions primer-service/src/Primer/OpenAPI.hs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{-# LANGUAGE StandaloneDeriving #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Primer.OpenAPI (
Expand All @@ -6,8 +7,12 @@ module Primer.OpenAPI (
) where

import Data.OpenApi (ToSchema)
import Data.Text (Text)
import Primer.API (Def, Prog, Tree)
import Primer.App (InitialApp)
import Primer.Core (ID (..))
import Primer.Database (Session, SessionName)
import Primer.Name (Name)

-- $orphanInstances
--
Expand All @@ -19,3 +24,14 @@ import Primer.Database (Session, SessionName)
instance ToSchema SessionName
instance ToSchema Session
instance ToSchema InitialApp

-- We need to GND the ID instance to match its To/FromJSON instances
deriving newtype instance ToSchema ID

-- We can't GND derive for Name as it is an opaque type
-- But the JSON instance is done by GND, so we must match here...
-- This instance works because the parameter has a phantom role!
deriving via Text instance (ToSchema Name)
brprice marked this conversation as resolved.
Show resolved Hide resolved
instance ToSchema Tree
instance ToSchema Def
instance ToSchema Prog
25 changes: 17 additions & 8 deletions primer-service/src/Primer/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import Primer.API (
renameSession,
variablesInScope,
)
import qualified Primer.API as API
import Primer.Action (
Action (Move),
ActionError (TypeError),
Expand Down Expand Up @@ -162,7 +163,11 @@ type PrimerOpenAPI =
:<|> QueryFlag "inMemory" :> "sessions" :>
PaginationParams :>
Summary "List sessions" :>
OpId "getSessionList" Get '[JSON] (Paginated Session))
OpId "getSessionList" Get '[JSON] (Paginated Session)

-- The rest of the API is scoped to a particular session
:<|> QueryParam' '[Required, Strict] "session" SessionId :> SOpenAPI
)

type PrimerLegacyAPI =
"api" :> (
Expand Down Expand Up @@ -193,15 +198,19 @@ type PrimerLegacyAPI =
:<|> Raw

-- | The session-specific bits of the api
type SAPI = (

type SOpenAPI = (
-- GET /api/program
brprice marked this conversation as resolved.
Show resolved Hide resolved
-- Get the current program state
"program" :> Get '[JSON] (Result ProgError Prog)
"program" :> Get '[JSON] API.Prog
)


-- | The session-specific bits of the api
-- (legacy version)
type SAPI = (
-- GET /api/session-name
-- Get the current session name.
:<|> "session-name" :> Get '[JSON] Text
"session-name" :> Get '[JSON] Text

-- PUT /api/session-name
-- Attempt to set the current session name. Returns the new
Expand Down Expand Up @@ -370,13 +379,13 @@ primerServer = openAPIServer :<|> legacyServer
where
openAPIServer =
newSession
:<|> \b p -> pagedDefaultClamp 100 p $ listSessions b
:<|> (\b p -> pagedDefaultClamp 100 p $ listSessions b)
:<|> getProgram
legacyServer =
( copySession
:<|> getVersion
:<|> ( \sid ->
getProgram sid
:<|> getSessionName sid
getSessionName sid
:<|> renameSession sid
:<|> edit sid
:<|> (variablesInScope sid :<|> generateNames sid)
Expand Down
1 change: 1 addition & 0 deletions primer/primer.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ test-suite primer-test
Tests.Action.Capture
Tests.Action.Prog
Tests.AlphaEquality
Tests.API
Tests.Database
Tests.Eval
Tests.EvalFull
Expand Down
120 changes: 114 additions & 6 deletions primer/src/Primer/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ module Primer.API (
copySession,
listSessions,
getVersion,
Tree,
Prog,
Def,
getProgram,
getSessionName,
renameSession,
Expand All @@ -28,6 +31,9 @@ module Primer.API (
evalStep,
evalFull,
flushSessions,
-- viewTree*: only exported for testing
viewTreeType,
viewTreeExpr,
) where

import Foreword
Expand All @@ -41,6 +47,10 @@ import Control.Concurrent.STM (
writeTBQueue,
)
import Control.Monad.Catch (MonadThrow, throwM)
import Data.Aeson (ToJSON)
import Data.Data (showConstr, toConstr)
import qualified Data.Generics.Uniplate.Data as U
import qualified Data.Map as Map
import qualified ListT (toList)
import Primer.App (
App,
Expand All @@ -51,7 +61,6 @@ import Primer.App (
EvalResp (..),
InitialApp,
MutationRequest,
Prog,
ProgError,
QueryAppM,
Question (..),
Expand All @@ -62,13 +71,25 @@ import Primer.App (
handleMutationRequest,
handleQuestion,
initialApp,
progDefs,
progTypes,
runEditAppM,
runQueryAppM,
)
import qualified Primer.App as App
import Primer.Core (
Expr,
Expr' (APP, Ann, GlobalVar, LetType, Letrec),
ID,
Kind,
Type',
Type,
Type' (TForall),
defExpr,
defID,
defName,
defType,
getID,
typeDefName,
)
import Primer.Database (
OffsetLimit,
Expand Down Expand Up @@ -97,7 +118,7 @@ import qualified Primer.Database as Database (
Success
),
)
import Primer.Name (Name)
import Primer.Name (Name, unName)
import qualified StmContainers.Map as StmMap

data Env = Env
Expand Down Expand Up @@ -256,10 +277,97 @@ liftEditAppM h sid = withSession' sid (EditApp $ runEditAppM h)
liftQueryAppM :: (MonadIO m, MonadThrow m) => QueryAppM a -> SessionId -> PrimerM m (Result ProgError a)
liftQueryAppM h sid = withSession' sid (QueryApp $ runQueryAppM h)

getProgram :: (MonadIO m, MonadThrow m) => SessionId -> PrimerM m (Result ProgError Prog)
getProgram = liftQueryAppM handleGetProgramRequest
getProgram :: (MonadIO m, MonadThrow m) => SessionId -> PrimerM m Prog
getProgram sid = withSession' sid $ QueryApp $ viewProg . handleGetProgramRequest

-- | A frontend will be mostly concerned with rendering, and does not need the
-- full complexity of our AST for that task. 'Tree' is a simplified view with
-- just enough information to render nicely.
-- (NB: currently this is just a first draft, and is expected to evolve.)
data Tree = Tree
{ nodeId :: ID
, label :: Text
, childTrees :: [Tree]
}
deriving (Show, Eq, Generic)

instance ToJSON Tree

-- | This type is the API's view of a 'App.Prog'
-- (this is expected to evolve as we flesh out the API)
data Prog = Prog
{ types :: [Name]
, -- We don't use Map ID Def, as the JSON encoding would be as an object,
-- where keys are IDs converted to strings and we have no nice way of
-- saying "all the keys of this object should parse as numbers". Similarly,
-- it is rather redundant as each Def carries a defID field (which is
-- encoded as a number), and it is difficult to enforce that "the keys of
-- this object match the defID field of the corresponding value".
defs :: [Def]
brprice marked this conversation as resolved.
Show resolved Hide resolved
}
deriving (Generic)

instance ToJSON Prog

-- | This type is the api's view of a 'Primer.Core.Def'
-- (this is expected to evolve as we flesh out the API)
data Def = Def
{ id :: ID
, name :: Name
, type_ :: Tree
, term :: Tree
}
deriving (Generic)

instance ToJSON Def

viewProg :: App.Prog -> Prog
viewProg p =
Prog
{ types = typeDefName <$> progTypes p
, defs =
( \d ->
Def
{ id = defID d
, name = defName d
, type_ = viewTreeType $ defType d
, term = viewTreeExpr $ defExpr d
}
)
<$> Map.elems (progDefs p)
}

-- | A simple method to extract 'Tree's from 'Expr's. This is injective.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a category theory-ish library on Hackage that we can use to wrap the Tree output type with an injective assertion/invariant?

-- Currently it is designed to be simple and just enough to enable
-- experimenting with rendering on the frontend.
--
-- It is expected to evolve in the future.
viewTreeExpr :: Expr -> Tree
viewTreeExpr = U.para $ \e exprChildren ->
let c = toS $ showConstr $ toConstr e
n = case e of
GlobalVar _ i -> c <> " " <> show i
_ -> unwords $ c : map unName (U.childrenBi e)
-- add info about type children
allChildren = case e of
Ann _ _ ty -> exprChildren ++ [viewTreeType ty]
APP _ _ ty -> exprChildren ++ [viewTreeType ty]
LetType _ _ ty _ -> viewTreeType ty : exprChildren
Letrec _ _ _ ty _ -> let (h, t) = splitAt 1 exprChildren in h ++ viewTreeType ty : t
-- otherwise, no type children
_ -> exprChildren
in Tree (getID e) n allChildren

-- | Similar to 'viewTreeExpr', but for 'Type's
viewTreeType :: Type -> Tree
viewTreeType = U.para $ \e allChildren ->
let c = toS $ showConstr $ toConstr e
n = case e of
TForall _ m k _ -> c <> " " <> unName m <> ":" <> show k
_ -> unwords $ c : map unName (U.childrenBi e)
in Tree (getID e) n allChildren

edit :: (MonadIO m, MonadThrow m) => SessionId -> MutationRequest -> PrimerM m (Result ProgError Prog)
edit :: (MonadIO m, MonadThrow m) => SessionId -> MutationRequest -> PrimerM m (Result ProgError App.Prog)
edit sid req = liftEditAppM (handleMutationRequest req) sid

variablesInScope :: (MonadIO m, MonadThrow m) => SessionId -> (ID, ID) -> PrimerM m (Result ProgError (([(Name, Kind)], [(Name, Type' ())]), [(ID, Name, Type' ())]))
Expand Down
2 changes: 1 addition & 1 deletion primer/src/Primer/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ focusNode prog defid nodeid =
Just x -> pure x

-- | Handle a request to retrieve the current program
handleGetProgramRequest :: MonadQueryApp m => m Prog
handleGetProgramRequest :: MonadReader App m => m Prog
handleGetProgramRequest = asks appProg

-- | Handle a request to mutate the app state
Expand Down
7 changes: 2 additions & 5 deletions primer/src/Primer/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,8 @@ newtype ID = ID {unID :: Int}
-- The Ord and Enum instances are useful for tests but we may remove them in
-- future, so don't use them in app code.
deriving newtype (Show, Num, Ord, Enum)
deriving (FromJSON, ToJSON) via VJSON ID

instance ToJSONKey ID

instance FromJSONKey ID
deriving newtype (FromJSON, ToJSON)
brprice marked this conversation as resolved.
Show resolved Hide resolved
deriving newtype (ToJSONKey, FromJSONKey)

data Meta a = Meta ID a (Maybe Value)
deriving (Generic, Eq, Show, Data, Functor)
Expand Down
2 changes: 1 addition & 1 deletion primer/src/Primer/Name.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import Primer.JSON
newtype Name = Name {unName :: Text}
deriving (Eq, Ord, Generic, Data)
deriving newtype (Show, IsString)
deriving (FromJSON, ToJSON) via VJSON Name
deriving newtype (FromJSON, ToJSON)
brprice marked this conversation as resolved.
Show resolved Hide resolved

-- | Construct a name from a Text. This is called unsafe because there are no
-- guarantees about whether the name refers to anything that is in scope.
Expand Down
Loading