Skip to content

Smithy4s integration #58

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

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b11823c
Wrote server/client interpreters
Baccata Feb 21, 2023
8b5795f
Example compiles
Baccata Feb 21, 2023
c18a814
Fixed un-bound flatMap, tried a few other things
Baccata Feb 21, 2023
619eb63
Merge remote-tracking branch 'origin/main' into smithy4s-integration
Baccata Feb 21, 2023
cce5d1d
Fix comment
Baccata Feb 21, 2023
0c3251b
Revert changes to FS2Channel
Baccata Feb 21, 2023
2cfb589
Bump remaining versions
Baccata Feb 21, 2023
4b8c0db
Merge branch 'main' into smithy4s-integration
ghostbuster91 May 3, 2025
6f93323
Merge branch 'main' into smithy4s-integration
ghostbuster91 May 4, 2025
53cee0a
Upgrade to latest smithy4s
ghostbuster91 May 4, 2025
fe670de
Fix scala native part of the build
ghostbuster91 May 5, 2025
799e818
Fix duplicated resources error
ghostbuster91 May 5, 2025
9523f61
Cross publish smithy across all platforms
ghostbuster91 May 5, 2025
67c49a5
Add versionScheme
ghostbuster91 May 5, 2025
703e25a
Convert for-comp to flatMap
ghostbuster91 May 5, 2025
0e7dd22
Add method on FS2Channel: resource (#80)
ghostbuster91 May 6, 2025
7d1323c
Fix smithy protocol wiring
kubukoz May 6, 2025
c3ce708
Merge pull request #81 from neandertech/fix-smithy-setup
ghostbuster91 May 6, 2025
6269973
Generate java parts for smithy traits
ghostbuster91 May 6, 2025
8aea9e6
Set minJdkVersion to 11
ghostbuster91 May 6, 2025
e45acf7
Remove unused file
ghostbuster91 May 6, 2025
97462b6
Simplify smithy4s implementation, get rid of fs2 in it
kubukoz May 7, 2025
70060bd
Merge branch 'smithy4s-integration' into smithy-simplify
kubukoz May 7, 2025
cb40262
bump smithy4s
kubukoz May 7, 2025
1ca96ee
Merge pull request #82 from neandertech/smithy-simplify
ghostbuster91 May 7, 2025
be15b68
Remove half-baked FutureBaseChannel
ghostbuster91 May 8, 2025
2ba2bab
Revert "Remove half-baked FutureBaseChannel"
ghostbuster91 May 8, 2025
5359542
feat: Replace jsoniter macros with circe (#83)
ghostbuster91 May 13, 2025
e46c197
Replace ChildProcess with fs2.Process
ghostbuster91 May 13, 2025
6c4b437
Add common smithy traits into protocol definition
ghostbuster91 May 13, 2025
830a6b0
Better way to coerce unit
ghostbuster91 May 14, 2025
c070f65
Errors smithy4s (#86)
ghostbuster91 May 15, 2025
bec1dca
Filter out optionals by default (#87)
kubukoz May 16, 2025
246ba77
Accessing endpoints from multiple servers
ghostbuster91 May 16, 2025
903ab82
chore: sort and group imports with scalafmt
ghostbuster91 May 16, 2025
3821de8
Add some scaladocs
ghostbuster91 May 16, 2025
fdb23b4
internal error when processing notification should not break the server
ghostbuster91 May 16, 2025
9395f05
Add smithy validations (#88)
ghostbuster91 May 17, 2025
a76d283
Fix RawMessage decoder
ghostbuster91 May 26, 2025
c3546cc
feat: Add jsonPayload trait (#89)
ghostbuster91 May 28, 2025
e168783
Rename traits to include Rpc in their names
ghostbuster91 May 29, 2025
074fb87
Update readme
ghostbuster91 May 29, 2025
8eee63e
Fix missing renames
ghostbuster91 May 29, 2025
381b083
Update README.md
ghostbuster91 Jun 7, 2025
55e611e
Update project/build.sbt
ghostbuster91 Jun 7, 2025
6f2f70c
Update project/plugins.sbt
ghostbuster91 Jun 7, 2025
1bc15ae
Apply review comments
ghostbuster91 Jun 7, 2025
9734e14
Replace target with release
ghostbuster91 Jun 7, 2025
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
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use flake
16 changes: 16 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
version = "3.8.0"
runner.dialect = scala213
maxColumn = 120

rewrite {
rules = [
ExpandImportSelectors,
Imports
]

imports {
groups = [
["[a-z].*"],
["java\\..*", "scala\\..*"]
]
sort = original
}
}

fileOverride {
"glob:**/fs2/src/**" {
runner.dialect = scala213source3
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,27 @@ override def ivyDeps = super.ivyDeps() ++ Agg(ivy"tech.neander::jsonrpclib-fs2::
**/!\ Please be aware that this library is in its early days and offers strictly no guarantee with regards to backward compatibility**

See the modules/examples folder.

## Smithy Integration

You can now use `jsonrpclib` directly with [Smithy](https://smithy.io/) and [smithy4s](https://disneystreaming.github.io/smithy4s/), enabling type-safe,
schema-first JSON-RPC APIs with minimal boilerplate.

This integration is supported by the following modules:

```scala
// Defines the Smithy protocol for JSON-RPC
libraryDependencies += "tech.neander" % "jsonrpclib-smithy" % <version>

// Provides smithy4s client/server bindings for JSON-RPC
libraryDependencies += "tech.neander" %%% "jsonrpclib-smithy4s" % <version>
```

With these modules, you can:

- Annotate your Smithy operations with `@jsonRpcRequest` or `@jsonRpcNotification`
- Generate client and server interfaces using smithy4s
- Use ClientStub to invoke remote services over JSON-RPC
- Use ServerEndpoints to expose service implementations via a Channel

This allows you to define your API once in Smithy and interact with it as a fully typed JSON-RPC service—without writing manual encoders, decoders, or dispatch logic.
196 changes: 171 additions & 25 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,46 @@ inThisBuild(
)

val scala213 = "2.13.16"
val scala3 = "3.3.5"
val scala3 = "3.3.6"
val jdkVersion = 11
val allScalaVersions = List(scala213, scala3)
val jvmScalaVersions = allScalaVersions
val jsScalaVersions = allScalaVersions
val nativeScalaVersions = allScalaVersions

val fs2Version = "3.12.0"

ThisBuild / versionScheme := Some("early-semver")
ThisBuild / tpolecatOptionsMode := DevMode

val commonSettings = Seq(
libraryDependencies ++= Seq(
"com.disneystreaming" %%% "weaver-cats" % "0.8.4" % Test
),
mimaPreviousArtifacts := Set(
organization.value %%% name.value % "0.0.7"
// organization.value %%% name.value % "0.0.7"
),
scalacOptions += "-java-output-version:8"
scalacOptions ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, _)) => Seq(s"-release:$jdkVersion")
case _ => Seq(s"-java-output-version:$jdkVersion")
}
},
)

val commonJvmSettings = Seq(
javacOptions ++= Seq("--release", jdkVersion.toString)
)

val core = projectMatrix
.in(file("modules") / "core")
.jvmPlatform(
jvmScalaVersions,
Test / unmanagedSourceDirectories ++= Seq(
(projectMatrixBaseDirectory.value / "src" / "test" / "scalajvm-native").getAbsoluteFile
)
Seq(
Test / unmanagedSourceDirectories ++= Seq(
(projectMatrixBaseDirectory.value / "src" / "test" / "scalajvm-native").getAbsoluteFile
)
) ++ commonJvmSettings
)
.jsPlatform(jsScalaVersions)
.nativePlatform(
Expand All @@ -56,13 +69,13 @@ val core = projectMatrix
name := "jsonrpclib-core",
commonSettings,
libraryDependencies ++= Seq(
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.2"
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-circe" % "2.30.2"
)
)

val fs2 = projectMatrix
.in(file("modules") / "fs2")
.jvmPlatform(jvmScalaVersions)
.jvmPlatform(jvmScalaVersions, commonJvmSettings)
.jsPlatform(jsScalaVersions)
.nativePlatform(nativeScalaVersions)
.disablePlugins(AssemblyPlugin)
Expand All @@ -71,19 +84,97 @@ val fs2 = projectMatrix
name := "jsonrpclib-fs2",
commonSettings,
libraryDependencies ++= Seq(
"co.fs2" %%% "fs2-core" % fs2Version
"co.fs2" %%% "fs2-core" % fs2Version,
"io.circe" %%% "circe-generic" % "0.14.7" % Test
)
)

val smithy = projectMatrix
.in(file("modules") / "smithy")
.jvmPlatform(false)
.disablePlugins(AssemblyPlugin, MimaPlugin)
.enablePlugins(SmithyTraitCodegenPlugin)
.settings(
name := "jsonrpclib-smithy",
commonJvmSettings,
smithyTraitCodegenDependencies := List(Dependencies.alloy.core),
smithyTraitCodegenJavaPackage := "jsonrpclib",
smithyTraitCodegenNamespace := "jsonrpclib"
)

val smithyTests = projectMatrix
.in(file("modules/smithy-tests"))
.jvmPlatform(Seq(scala213))
.dependsOn(smithy)
.settings(
publish / skip := true,
libraryDependencies ++= Seq(
"com.disneystreaming" %%% "weaver-cats" % "0.8.4" % Test
)
)
.disablePlugins(MimaPlugin)

lazy val buildTimeProtocolDependency =
/** By default, smithy4sInternalDependenciesAsJars doesn't contain the jars in the "smithy4s" configuration. We have
* to add them manually - this is the equivalent of a "% Smithy4s"-scoped dependency.
*
* Ideally, this would be
* {{{
* (Compile / smithy4sInternalDependenciesAsJars) ++=
* Smithy4s / smithy4sInternalDependenciesAsJars).value.map(_.data)
* }}}
*
* but that doesn't work because the Smithy4s configuration doesn't extend from Compile so it doesn't have the
* `internalDependencyAsJars` setting.
*/
Compile / smithy4sInternalDependenciesAsJars ++=
(smithy.jvm(autoScalaLibrary = false) / Compile / fullClasspathAsJars).value.map(_.data)

val smithy4s = projectMatrix
.in(file("modules") / "smithy4s")
.jvmPlatform(jvmScalaVersions, commonJvmSettings)
.jsPlatform(jsScalaVersions)
.nativePlatform(Seq(scala3))
.disablePlugins(AssemblyPlugin)
.enablePlugins(Smithy4sCodegenPlugin)
.dependsOn(core)
.settings(
name := "jsonrpclib-smithy4s",
commonSettings,
mimaPreviousArtifacts := Set.empty,
libraryDependencies ++= Seq(
"com.disneystreaming.smithy4s" %%% "smithy4s-json" % smithy4sVersion.value
),
buildTimeProtocolDependency
)

val smithy4sTests = projectMatrix
.in(file("modules") / "smithy4s-tests")
.jvmPlatform(jvmScalaVersions, commonJvmSettings)
.jsPlatform(jsScalaVersions)
.nativePlatform(Seq(scala3))
.disablePlugins(AssemblyPlugin)
.enablePlugins(Smithy4sCodegenPlugin)
.dependsOn(smithy4s, fs2 % Test)
.settings(
commonSettings,
publish / skip := true,
libraryDependencies ++= Seq(
"io.circe" %%% "circe-generic" % "0.14.7"
),
buildTimeProtocolDependency
)

val exampleServer = projectMatrix
.in(file("modules") / "examples/server")
.jvmPlatform(List(scala213))
.jvmPlatform(List(scala213), commonJvmSettings)
.dependsOn(fs2)
.settings(
commonSettings,
publish / skip := true,
libraryDependencies ++= Seq(
"co.fs2" %%% "fs2-io" % fs2Version
"co.fs2" %%% "fs2-io" % fs2Version,
"io.circe" %%% "circe-generic" % "0.14.7"
)
)
.disablePlugins(MimaPlugin)
Expand All @@ -95,38 +186,93 @@ val exampleClient = projectMatrix
Seq(
fork := true,
envVars += "SERVER_JAR" -> (exampleServer.jvm(scala213) / assembly).value.toString
)
) ++ commonJvmSettings
)
.disablePlugins(AssemblyPlugin)
.dependsOn(fs2)
.settings(
commonSettings,
publish / skip := true,
libraryDependencies ++= Seq(
"co.fs2" %%% "fs2-io" % fs2Version
"co.fs2" %%% "fs2-io" % fs2Version,
"io.circe" %%% "circe-generic" % "0.14.7"
)
)
.disablePlugins(MimaPlugin)

val exampleSmithyShared = projectMatrix
.in(file("modules") / "examples/smithyShared")
.jvmPlatform(List(scala213), commonJvmSettings)
.dependsOn(smithy4s, fs2)
.enablePlugins(Smithy4sCodegenPlugin)
.settings(
commonSettings,
publish / skip := true,
buildTimeProtocolDependency
)
.disablePlugins(MimaPlugin)

val exampleSmithyServer = projectMatrix
.in(file("modules") / "examples/smithyServer")
.jvmPlatform(List(scala213), commonJvmSettings)
.dependsOn(exampleSmithyShared)
.settings(
commonSettings,
publish / skip := true,
libraryDependencies ++= Seq(
"co.fs2" %%% "fs2-io" % fs2Version
),
assembly / assemblyMergeStrategy := {
case PathList("META-INF", "smithy", _*) => MergeStrategy.concat
case PathList("jsonrpclib", "package.class") => MergeStrategy.first
case PathList("META-INF", xs @ _*) if xs.nonEmpty => MergeStrategy.discard
case x => MergeStrategy.first
}
)
.disablePlugins(MimaPlugin)

val exampleSmithyClient = projectMatrix
.in(file("modules") / "examples/smithyClient")
.jvmPlatform(
List(scala213),
Seq(
fork := true,
envVars += "SERVER_JAR" -> (exampleSmithyServer.jvm(scala213) / assembly).value.toString
) ++ commonJvmSettings
)
.dependsOn(exampleSmithyShared)
.settings(
commonSettings,
publish / skip := true,
libraryDependencies ++= Seq(
"co.fs2" %%% "fs2-io" % fs2Version
)
)
.disablePlugins(MimaPlugin, AssemblyPlugin)

val root = project
.in(file("."))
.settings(
publish / skip := true
)
.disablePlugins(MimaPlugin, AssemblyPlugin)
.aggregate(List(core, fs2, exampleServer, exampleClient).flatMap(_.projectRefs): _*)

// The core compiles are a workaround for https://github.com/plokhotnyuk/jsoniter-scala/issues/564
// when we switch to SN 0.5, we can use `makeWithSkipNestedOptionValues` instead: https://github.com/plokhotnyuk/jsoniter-scala/issues/564#issuecomment-2787096068
val compileCoreModules = {
for {
scalaVersionSuffix <- List("", "3")
platformSuffix <- List("", "JS", "Native")
task <- List("compile", "package")
} yield s"core$platformSuffix$scalaVersionSuffix/$task"
}.mkString(";")
.aggregate(
List(
core,
fs2,
exampleServer,
exampleClient,
smithy,
smithyTests,
smithy4s,
smithy4sTests,
exampleSmithyShared,
exampleSmithyServer,
exampleSmithyClient
).flatMap(_.projectRefs): _*
)

addCommandAlias(
"ci",
s"$compileCoreModules;test;scalafmtCheckAll;mimaReportBinaryIssues"
s"compile;test;scalafmtCheckAll;mimaReportBinaryIssues"
)
Loading