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

Ignore non-model JSON imports #290

Merged
merged 4 commits into from
Jun 9, 2023
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
14 changes: 0 additions & 14 deletions modules/core/src/main/scala/playground/ModelReader.scala

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ object PlaygroundConfig {
}

object BuildConfig {
implicit val c: JsonValueCodec[BuildConfig] = JsonCodecMaker.make[BuildConfig]
implicit val c: JsonValueCodec[BuildConfig] = JsonCodecMaker.make

def fromPlaygroundConfig(
c: PlaygroundConfig
Expand Down
76 changes: 54 additions & 22 deletions modules/lsp/src/main/scala/playground/lsp/BuildLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package playground.lsp

import cats.effect.kernel.Sync
import cats.implicits._
import fs2.io.file.Files
import fs2.io.file.Path
import playground.ModelReader
import playground.PlaygroundConfig
import playground.language.TextDocumentProvider
import playground.language.Uri
import playground.lsp.util.SerializedSmithyModel
import smithy4s.dynamic.DynamicSchemaIndex

trait BuildLoader[F[_]] {
Expand Down Expand Up @@ -37,7 +38,7 @@ object BuildLoader {
val default: Loaded = Loaded(PlaygroundConfig.empty, Path("/"))
}

def instance[F[_]: TextDocumentProvider: Sync]: BuildLoader[F] =
def instance[F[_]: TextDocumentProvider: Sync: Files]: BuildLoader[F] =
new BuildLoader[F] {

def load(
Expand Down Expand Up @@ -87,28 +88,59 @@ object BuildLoader {

def buildSchemaIndex(
loaded: BuildLoader.Loaded
): F[DynamicSchemaIndex] = Sync[F]
): F[DynamicSchemaIndex] = {
// This has to be lazy, because for the default, "no imports" config, the file path points to the filesystem root.
lazy val workspaceBase = loaded
.configFilePath
.parent
.getOrElse(sys.error("impossible - no parent for " + loaded.configFilePath))

// "raw" means these can be directories etc., just like in the config file.
val rawImportPaths = loaded.config.imports.map(workspaceBase.resolve).toSet

for {
specs <- filterImports(rawImportPaths)
model <- loadModel(specs, loaded.config)
dsi <- DynamicSchemaIndex.loadModel(model).liftTo[F]
} yield dsi
}

private def loadModel(
specs: Set[Path],
config: PlaygroundConfig,
) = Sync[F]
.interruptibleMany {
ModelLoader
.load(
specs =
loaded
.config
.imports
.map(
loaded
.configFilePath
.parent
.getOrElse(sys.error("impossible - no parent"))
.resolve(_)
.toNioPath
.toFile()
)
.toSet,
jars = ModelLoader.resolveModelDependencies(loaded.config),
)
ModelLoader.load(
specs = specs.map(_.toNioPath.toFile),
jars = ModelLoader.resolveModelDependencies(config),
)
}
.flatMap(ModelReader.buildSchemaIndex[F])

private def filterImports(
specs: Set[Path]
): F[Set[Path]] = fs2
.Stream
.emits(specs.toSeq)
.flatMap(Files[F].walk(_))
.evalFilterNot(Files[F].isDirectory)
.evalFilter { file =>
val isSmithyFile = file.extName === ".smithy"

if (isSmithyFile)
true.pure[F]
else
isSerializedSmithyModelF(file)
}
.compile
.to(Set)

private def isSerializedSmithyModelF(
file: Path
): F[Boolean] = Files[F]
.readAll(file)
.compile
.to(Array)
.map(SerializedSmithyModel.decode(_).isRight)

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package playground.lsp.util

import cats.implicits._

case class SerializedSmithyModel(
smithy: String
)

object SerializedSmithyModel {
import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker

private implicit val c: JsonValueCodec[SerializedSmithyModel] = JsonCodecMaker.make

val decode: Array[Byte] => Either[Throwable, SerializedSmithyModel] =
bytes =>
Either
.catchNonFatal(readFromArray[SerializedSmithyModel](bytes))

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use service weather#WeatherService
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"imports": ["src"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"smithy": "2.0",
"shapes": {
"weather#GetWeather": {
"type": "operation",
"input": {
"target": "weather#GetWeatherInput"
},
"output": {
"target": "weather#GetWeatherOutput"
},
"traits": {
"smithy.api#readonly": {}
}
},
"weather#GetWeatherInput": {
"type": "structure",
"members": {
"city": {
"target": "smithy.api#String",
"traits": {
"smithy.api#httpLabel": {},
"smithy.api#required": {}
}
}
},
"traits": {
"smithy.api#input": {}
}
},
"weather#GetWeatherOutput": {
"type": "structure",
"members": {
"weather": {
"target": "weather#Weather",
"traits": {
"smithy.api#required": {}
}
}
},
"traits": {
"smithy.api#output": {}
}
},
"weather#GoodWeather": {
"type": "structure",
"members": {
"reallyGood": {
"target": "smithy.api#Boolean"
}
}
},
"weather#Weather": {
"type": "union",
"members": {
"good": {
"target": "weather#GoodWeather"
},
"decent": {
"target": "smithy.api#Unit"
}
}
},
"weather#WeatherService": {
"type": "service",
"operations": [
{
"target": "weather#GetWeather"
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"imports": ["src"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"this": "is not a Smithy model file"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ import fs2.io.file.Files
import fs2.io.file.Path
import org.eclipse.lsp4j.CodeLensParams
import org.eclipse.lsp4j.DidChangeWatchedFilesParams
import org.eclipse.lsp4j.DocumentDiagnosticParams
import org.eclipse.lsp4j.MessageType
import org.eclipse.lsp4j.TextDocumentIdentifier
import playground.PlaygroundConfig
import playground.language.Uri
import playground.lsp.harness.LanguageServerIntegrationTests
import playground.lsp.harness.TestClient.MessageLog
import weaver._

import scala.jdk.CollectionConverters._

object LanguageServerReloadIntegrationTests
extends SimpleIOSuite
with LanguageServerIntegrationTests {
Expand Down Expand Up @@ -82,15 +87,15 @@ object LanguageServerReloadIntegrationTests
}

getLenses.flatMap { lensesBefore =>
assert.same(lensesBefore, Nil).failFast[IO]
assert(lensesBefore.isEmpty).failFast[IO]
} *>
addLibrary *>
f.server.didChangeWatchedFiles(new DidChangeWatchedFilesParams()) *>
getLenses
}
}
.use { lensesAfter =>
assert.same(lensesAfter.length, 1).pure[IO]
assert(lensesAfter.length == 1).pure[IO]
}
}

Expand All @@ -112,4 +117,40 @@ object LanguageServerReloadIntegrationTests
.use_
.as(success)
}

test("workspace can be loaded even if non-model JSON files are included") {
makeServer(testWorkspacesBase / "non-model-jsons")
.use(_.client.getEvents)
.map { events =>
val errorLogs = events.collect { case MessageLog(MessageType.Error, msg) => msg }
assert(errorLogs.isEmpty)
}
}

test("JSON smithy models can be loaded") {
makeServer(testWorkspacesBase / "json-models")
.use { f =>
f.client.getEvents.flatMap { events =>
val errorLogs = events.collect { case MessageLog(MessageType.Error, msg) => msg }

f.server
.diagnostic(
new DocumentDiagnosticParams(
new TextDocumentIdentifier((f.workspaceDir / "input.smithyql").value)
)
)
.map { diags =>
val items = diags
.getRelatedFullDocumentDiagnosticReport()
.getItems()
.asScala
.toList
.map(_.getMessage())

assert(errorLogs.isEmpty) &&
assert(items.isEmpty)
}
}
}
}
}