Add a method for getting component groups without execution context (#7569)

close #7510

Changelog:
- add: `runtime/getComponentGroups` request returning the component groups currently available in runtime
This commit is contained in:
Dmitry Bushev 2023-08-14 17:49:16 +01:00 committed by GitHub
parent d3436fae70
commit 060323e511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 281 additions and 6 deletions

View File

@ -182,6 +182,8 @@ transport formats, please look [here](./protocol-architecture).
- [`library/getPackage`](#librarygetpackage)
- [`library/publish`](#librarypublish)
- [`library/preinstall`](#librarypreinstall)
- [Runtime Operations](#runtime-operations)
- [`runtime/getComponentGroups`](#runtime-getcomponentgroups)
- [Errors](#errors-75)
- [`Error`](#error)
- [`AccessDeniedError`](#accessdeniederror)
@ -3888,12 +3890,10 @@ null;
Sent from the client to the server to get the list of component groups available
in runtime.
The engine is started with an empty list of libraries loaded. It means that the
request should be sent after the first
[`executionContext/executionComplete`](#executioncontextexecutioncomplete)
notification indicating that all the libraries are loaded, and the component
group list is populated. If the request is sent before the first notification,
the response may be empty or not contain all available components.
#### Deprecated
The request is deprecated in favor of
[`runtime/getComponentGroups`](#runtime-getcomponentgroups).
- **Type:** Request
- **Direction:** Client -> Server
@ -5333,6 +5333,43 @@ null;
- [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable
file-system error.
## Runtime Operations
### `runtime/getComponentGroups`
Sent from the client to the server to get the list of component groups available
in runtime.
The engine is started with an empty list of libraries loaded. It means that the
request should be sent after the first
[`executionContext/executionComplete`](#executioncontextexecutioncomplete)
notification indicating that all the libraries are loaded, and the component
group list is populated. If the request is sent before the first notification,
the response may be empty or not contain all available components.
- **Type:** Request
- **Direction:** Client -> Server
- **Connection:** Protocol
- **Visibility:** Public
#### Parameters
```typescript
null;
```
#### Result
```typescript
{
componentGroups: LibraryComponentGroup[];
}
```
#### Errors
None
## Errors
The language server component also has its own set of errors. This section is

View File

@ -57,6 +57,7 @@ import org.enso.languageserver.requesthandler.visualization.{
import org.enso.languageserver.requesthandler.workspace.ProjectInfoHandler
import org.enso.languageserver.runtime.ContextRegistryProtocol
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.runtime.RuntimeApi.RuntimeGetComponentGroups
import org.enso.languageserver.runtime.VisualizationApi.{
AttachVisualization,
DetachVisualization,
@ -584,6 +585,10 @@ class JsonConnectionController(
RenameSymbol -> RenameSymbolHandler.props(
requestTimeout,
runtimeConnector
),
RuntimeGetComponentGroups -> runtime.GetComponentGroupsHandler.props(
requestTimeout,
runtimeConnector
)
)
}

View File

@ -24,6 +24,7 @@ import org.enso.languageserver.runtime.VisualizationApi._
import org.enso.languageserver.session.SessionApi.InitProtocolConnection
import org.enso.languageserver.text.TextApi._
import org.enso.languageserver.libraries.LibraryApi._
import org.enso.languageserver.runtime.RuntimeApi.RuntimeGetComponentGroups
import org.enso.languageserver.vcsmanager.VcsManagerApi._
import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo
@ -100,6 +101,7 @@ object JsonRpc {
.registerRequest(LibraryGetPackage)
.registerRequest(LibraryPublish)
.registerRequest(LibraryPreinstall)
.registerRequest(RuntimeGetComponentGroups)
.registerNotification(TaskStarted)
.registerNotification(TaskProgressUpdate)
.registerNotification(TaskFinished)

View File

@ -0,0 +1,146 @@
package org.enso.languageserver.requesthandler.runtime
import akka.actor.{Actor, ActorRef, Cancellable, Props}
import com.typesafe.scalalogging.LazyLogging
import org.enso.editions.LibraryName
import org.enso.jsonrpc._
import org.enso.languageserver.libraries.{
ComponentGroupsResolver,
ComponentGroupsValidator,
LibraryComponentGroup
}
import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.RuntimeApi._
import org.enso.languageserver.util.UnhandledLogging
import org.enso.pkg.ComponentGroups
import org.enso.polyglot.runtime.Runtime.Api
import java.util.UUID
import scala.collection.immutable.ListMap
import scala.concurrent.duration.FiniteDuration
/** A request handler for `runtime/getComponentGroups` commands.
*
* @param timeout request timeout
* @param runtime a reference to the runtime connector
* @param componentGroupsResolver resolves dependencies between the component
* groups of different packages
* @param componentGroupsValidator validates the component groups
*/
class GetComponentGroupsHandler(
timeout: FiniteDuration,
runtime: ActorRef,
componentGroupsResolver: ComponentGroupsResolver,
componentGroupsValidator: ComponentGroupsValidator
) extends Actor
with LazyLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = {
case Request(
RuntimeGetComponentGroups,
id,
_
) =>
runtime ! Api.Request(UUID.randomUUID(), Api.GetComponentGroupsRequest())
val cancellable =
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
context.become(responseStage(id, sender(), cancellable))
}
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case RequestTimeout =>
logger.error("Request [{}] timed out.", id)
replyTo ! ResponseError(Some(id), Errors.RequestTimeout)
context.stop(self)
case Api.Response(_, Api.GetComponentGroupsResponse(componentGroups)) =>
replyTo ! ResponseResult(
RuntimeGetComponentGroups,
id,
RuntimeGetComponentGroups.Result(
resolveComponentGroups(componentGroups.to(ListMap))
)
)
cancellable.cancel()
context.stop(self)
}
private def resolveComponentGroups(
componentGroups: Map[LibraryName, ComponentGroups]
): Seq[LibraryComponentGroup] = {
val validated = componentGroupsValidator.validate(componentGroups)
validated.collect { case (_, Left(error)) =>
logValidationError(error)
}
val validatedComponents = validated
.collect { case (libraryName, Right(componentGroups)) =>
libraryName -> componentGroups
}
componentGroupsResolver.resolveComponentGroups(validatedComponents)
}
private def logValidationError(
error: ComponentGroupsValidator.ValidationError
): Unit =
error match {
case ComponentGroupsValidator.ValidationError
.InvalidComponentGroups(libraryName, message) =>
logger.warn(
s"Validation error. Failed to read library [$libraryName] " +
s"component groups (reason: $message)."
)
case ComponentGroupsValidator.ValidationError
.DuplicatedComponentGroup(libraryName, moduleReference) =>
logger.warn(
s"Validation error. Library [$libraryName] defines duplicate " +
s"component group [$moduleReference]."
)
case ComponentGroupsValidator.ValidationError
.ComponentGroupExtendsNothing(libraryName, moduleReference) =>
logger.warn(
s"Validation error. Library [$libraryName] component group " +
s"[$moduleReference] extends nothing."
)
}
}
object GetComponentGroupsHandler {
/** Creates configuration object used to create a
* [[GetComponentGroupsHandler]].
*
* @param timeout request timeout
* @param runtime a reference to the runtime connector
* @param componentGroupsResolver resolves dependencies between the component
* groups of different packages
* @param componentGroupsValidator validates the component groups
*/
def props(
timeout: FiniteDuration,
runtime: ActorRef,
componentGroupsResolver: ComponentGroupsResolver =
new ComponentGroupsResolver,
componentGroupsValidator: ComponentGroupsValidator =
new ComponentGroupsValidator
): Props =
Props(
new GetComponentGroupsHandler(
timeout,
runtime,
componentGroupsResolver,
componentGroupsValidator
)
)
}

View File

@ -0,0 +1,23 @@
package org.enso.languageserver.runtime
import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused}
import org.enso.languageserver.libraries.LibraryComponentGroup
object RuntimeApi {
case object RuntimeGetComponentGroups
extends Method("runtime/getComponentGroups") {
case class Result(componentGroups: Seq[LibraryComponentGroup])
implicit val hasParams: HasParams.Aux[this.type, Unused.type] =
new HasParams[this.type] {
type Params = Unused.type
}
implicit val hasResult
: HasResult.Aux[this.type, RuntimeGetComponentGroups.Result] =
new HasResult[this.type] {
type Result = RuntimeGetComponentGroups.Result
}
}
}

View File

@ -0,0 +1,62 @@
package org.enso.languageserver.websocket.json
import io.circe.literal._
import org.enso.languageserver.runtime.TestComponentGroups
import org.enso.polyglot.runtime.Runtime.Api
class RuntimeTest extends BaseServerTest {
"runtime/getComponentGroups" should {
"return component groups successfully" in {
val client = getInitialisedWsClient()
// get component groups
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "runtime/getComponentGroups",
"id": 1,
"params": null
}
"""
)
val requestId =
runtimeConnectorProbe.receiveN(1).head match {
case Api.Request(requestId, Api.GetComponentGroupsRequest()) =>
requestId
case msg =>
fail(s"Unexpected message: $msg")
}
runtimeConnectorProbe.lastSender ! Api.Response(
requestId,
Api.GetComponentGroupsResponse(
TestComponentGroups.standardBase.toVector
)
)
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 1,
"result": {
"componentGroups": [
{
"library" : "Standard.Base",
"name" : "Input",
"exports" : [
{
"name" : "Standard.Base.File.new"
},
{
"name" : "Standard.Database.Connection.Database.connect"
}
]
}
]
}
}
""")
}
}
}