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

Add error-related transformations + documentation #584

Merged
merged 11 commits into from
Nov 10, 2022
Merged
92 changes: 69 additions & 23 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import _root_.java.util.stream.Collectors
import java.nio.file.Files
import sbt.internal.IvyConsole
import org.scalajs.jsenv.nodejs.NodeJSEnv

Expand All @@ -12,7 +14,7 @@ ThisBuild / mimaBaseVersion := "0.17.0"

Global / onChangedBuildSource := ReloadOnSourceChanges

import Smithy4sPlugin._
import Smithy4sBuildPlugin._

val latest2ScalaVersions = List(Scala213, Scala3)
val allJvmScalaVersions = List(Scala212, Scala213, Scala3)
Expand All @@ -39,7 +41,7 @@ lazy val root = project
.aggregate(allModules: _*)
// .disablePlugins(Smithy4sPlugin)
.enablePlugins(ScalafixPlugin)
.settings(Smithy4sPlugin.doNotPublishArtifact)
.settings(Smithy4sBuildPlugin.doNotPublishArtifact)
.settings(
pushRemoteCache := {},
pullRemoteCache := {},
Expand Down Expand Up @@ -81,10 +83,10 @@ lazy val docs =
`http4s-swagger`,
decline,
`aws-http4s` % "compile -> compile,test",
complianceTests % "compile -> compile,test"
complianceTests
)
.settings(
mdocIn := (ThisBuild / baseDirectory).value / "modules" / "docs" / "src",
mdocIn := (ThisBuild / baseDirectory).value / "modules" / "docs" / "markdown",
mdocVariables := Map(
"VERSION" -> {
sys.env
Expand All @@ -109,12 +111,16 @@ lazy val docs =
Dependencies.Http4s.emberServer.value,
Dependencies.Decline.effect.value
),
Compile / genSmithyDependencies ++= Seq(Dependencies.Smithy.testTraits),
Compile / sourceGenerators := Seq(genSmithyScala(Compile).taskValue),
Compile / smithySpecs := Seq(
(ThisBuild / baseDirectory).value / "sampleSpecs" / "hello.smithy"
(Compile / sourceDirectory).value / "smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "test.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "hello.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "kvstore.smithy"
)
)
.settings(Smithy4sPlugin.doNotPublishArtifact)
.settings(Smithy4sBuildPlugin.doNotPublishArtifact)

val munitDeps = Def.setting {
if (virtualAxes.value.contains(VirtualAxis.native)) {
Expand Down Expand Up @@ -205,6 +211,7 @@ lazy val core = projectMatrix
(ThisBuild / baseDirectory).value / "sampleSpecs" / "reservednames.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "enums.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "defaults.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "kvstore.smithy",
(ThisBuild / baseDirectory).value / "sampleSpecs" / "errorHandling.smithy"
),
(Test / sourceGenerators) := Seq(genSmithyScala(Test).taskValue),
Expand Down Expand Up @@ -519,7 +526,7 @@ lazy val protocolTests = projectMatrix
Dependencies.Weaver.scalacheck.value % Test
)
)
.settings(Smithy4sPlugin.doNotPublishArtifact)
.settings(Smithy4sBuildPlugin.doNotPublishArtifact)

/**
* This modules contains utilities to dynamically instantiate
Expand All @@ -539,12 +546,7 @@ lazy val dynamic = projectMatrix
Compile / smithySpecs := Seq(
(ThisBuild / baseDirectory).value / "modules" / "dynamic" / "smithy" / "dynamic.smithy"
),
Compile / sourceGenerators := Seq(genSmithyScala(Compile).taskValue),
Test / allowedNamespaces := Seq("smithy4s.example"),
Test / smithySpecs := Seq(
(ThisBuild / baseDirectory).value / "sampleSpecs" / "kvstore.smithy"
),
(Test / sourceGenerators) := Seq(genSmithyScala(Test).taskValue)
Compile / sourceGenerators := Seq(genSmithyScala(Compile).taskValue)
)
.jvmPlatform(
allJvmScalaVersions,
Expand Down Expand Up @@ -640,7 +642,7 @@ lazy val `http4s-swagger` = projectMatrix
lazy val testUtils = projectMatrix
.in(file("modules/test-utils"))
.dependsOn(core)
.settings(Smithy4sPlugin.doNotPublishArtifact)
.settings(Smithy4sBuildPlugin.doNotPublishArtifact)
.settings(
libraryDependencies += Dependencies.Cats.core.value
)
Expand Down Expand Up @@ -692,7 +694,7 @@ lazy val complianceTests = projectMatrix
.dependsOn(core, http4s % "compile->compile; test->compile", testUtils)
.settings(
name := "compliance-tests",
Compile / allowedNamespaces := Seq("smithy.test", "smithy4s.example"),
Compile / allowedNamespaces := Seq("smithy.test", "smithy4s.example.test"),
Compile / genSmithyDependencies ++= Seq(Dependencies.Smithy.testTraits),
Compile / sourceGenerators := Seq(genSmithyScala(Compile).taskValue),
isCE3 := virtualAxes.value.contains(CatsEffect3Axis),
Expand Down Expand Up @@ -760,7 +762,7 @@ lazy val example = projectMatrix
smithy4sSkip := List("resource")
)
.jvmPlatform(List(Scala213), jvmDimSettings)
.settings(Smithy4sPlugin.doNotPublishArtifact)
.settings(Smithy4sBuildPlugin.doNotPublishArtifact)

lazy val guides = projectMatrix
.in(file("modules/guides"))
Expand All @@ -778,7 +780,7 @@ lazy val guides = projectMatrix
)
)
.jvmPlatform(Seq(Scala3), jvmDimSettings)
.settings(Smithy4sPlugin.doNotPublishArtifact)
.settings(Smithy4sBuildPlugin.doNotPublishArtifact)

/**
* Pretty primitive benchmarks to test that we're not doing anything drastically
Expand All @@ -801,7 +803,7 @@ lazy val benchmark = projectMatrix
(Compile / sourceGenerators) := Seq(genSmithyScala(Compile).taskValue)
)
.jvmPlatform(List(Scala213), jvmDimSettings)
.settings(Smithy4sPlugin.doNotPublishArtifact)
.settings(Smithy4sBuildPlugin.doNotPublishArtifact)

val isCE3 = settingKey[Boolean]("Is the current build using CE3?")

Expand Down Expand Up @@ -968,21 +970,66 @@ def genSmithyImpl(config: Configuration) = Def.task {
}

val codegenCp =
(`codegen-cli`.jvm(Smithy4sPlugin.Scala213) / Compile / fullClasspath).value
(`codegen-cli`.jvm(
Smithy4sBuildPlugin.Scala213
) / Compile / fullClasspath).value
.map(_.data)

val mc = "smithy4s.codegen.cli.Main"
val s = (config / streams).value

def untupled[A, B, C](f: ((A, B)) => C): (A, B) => C = (a, b) => f((a, b))

import sjsonnew._
import BasicJsonProtocol._
import sbt.FileInfo
import sbt.HashFileInfo
import sbt.io.Hash
import scala.jdk.CollectionConverters._

// Json codecs used by SBT's caching constructs
// This serialises a path by providing a hash of the content it points to.
// Because the hash is part of the Json, this allows SBT to detect when a file
// changes and invalidate its relevant caches, leading to a call to Smithy4s' code generator.
implicit val pathFormat: JsonFormat[File] =
BasicJsonProtocol.projectFormat[File, HashFileInfo](
p => {
if (p.isFile()) FileInfo.hash(p)
else
// If the path is a directory, we get the hashes of all files
// then hash the concatenation of the hash's bytes.
Comment on lines +999 to +1000
Copy link
Member

Choose a reason for hiding this comment

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

this looks too familiar

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's because it's also in the SBT plugin built by this project.

Copy link
Member

Choose a reason for hiding this comment

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

Yep

FileInfo.hash(
p,
Hash(
Files
.walk(p.toPath(), 2)
.collect(Collectors.toList())
.asScala
.map(_.toFile())
.map(Hash(_))
.foldLeft(Array.emptyByteArray)(_ ++ _)
)
)
},
hash => hash.file
)

case class CodegenInput(files: Seq[File])
object CodegenInput {
implicit val seqFormat: JsonFormat[CodegenInput] =
BasicJsonProtocol.projectFormat[CodegenInput, Seq[File]](
input => input.files,
files => CodegenInput(files)
)(BasicJsonProtocol.seqFormat(pathFormat))
}

val cached =
Tracked.inputChanged[FilesInfo[HashFileInfo], Seq[File]](
Tracked.inputChanged[CodegenInput, Seq[File]](
s.cacheStoreFactory.make("input")
) {
untupled {
Tracked
.lastOutput[(Boolean, FilesInfo[HashFileInfo]), Seq[File]](
.lastOutput[(Boolean, CodegenInput), Seq[File]](
s.cacheStoreFactory.make("output")
) { case ((changed, files), outputs) =>
if (changed || outputs.isEmpty) {
Expand Down Expand Up @@ -1019,8 +1066,7 @@ def genSmithyImpl(config: Configuration) = Def.task {
}

val trackedFiles = inputFiles ++ codegenCp.allPaths.get()
cached(FilesInfo(trackedFiles.map(FileInfo.hash(_)).toSet))
.partition(_.ext == "scala")
cached(CodegenInput(trackedFiles)).partition(_.ext == "scala")
}

addCommandAlias(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,14 +259,15 @@ object CollisionAvoidance {

def getReservedNames: Set[String] = reservedNames

val Transformation = NameRef("smithy4s.capability", "Transformation")
val Transformation = NameRef("smithy4s", "Transformation")
val PolyFunction5_ = NameRef("smithy4s.kinds", "PolyFunction5")
val Service_ = NameRef("smithy4s", "Service")
val Endpoint_ = NameRef("smithy4s", "Endpoint")
val NoInput_ = NameRef("smithy4s", "NoInput")
val ShapeId_ = NameRef("smithy4s", "ShapeId")
val Schema_ = NameRef("smithy4s", "Schema")
val FunctorAlgebra_ = NameRef("smithy4s.kinds", "FunctorAlgebra")
val BiFunctorAlgebra_ = NameRef("smithy4s.kinds", "BiFunctorAlgebra")
val StreamingSchema_ = NameRef("smithy4s", "StreamingSchema")
val Enumeration_ = NameRef("smithy4s", "Enumeration")
val EnumValue_ = NameRef("smithy4s.schema", "EnumValue")
Expand Down
10 changes: 3 additions & 7 deletions modules/codegen/src/smithy4s/codegen/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
val nameGen = NameRef(s"${name}Gen")
lines(
line"type ${NameDef(name)}[F[_]] = $FunctorAlgebra_[$nameGen, F]",
block(
line"object ${NameRef(name)} extends $Service_.Provider[$nameGen]"
)(
line"def apply[F[_]](implicit F: ${NameRef(name)}[F]): F.type = F",
line"def service: $Service_[$nameGen] = $nameGen",
line"val id: $ShapeId_ = service.id"
)
line"val ${NameRef(name)} = $nameGen"
)
case _ => Lines.empty
}
Expand Down Expand Up @@ -210,6 +204,8 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
newline,
line"def apply[F[_]](implicit F: $FunctorAlgebra_[$genNameRef, F]): F.type = F",
newline,
line"type WithError[F[_, _]] = $BiFunctorAlgebra_[$genNameRef, F]",
newline,
renderId(shapeId),
newline,
renderHintsVal(hints),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import cats.effect.IO
import cats.effect.Resource
import org.http4s._
import org.http4s.client.Client
import smithy4s.example._
import smithy4s.example.test._
import smithy4s.http4s._
import weaver._
import smithy4s.Service
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/generated/kinds/polyFunctions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
package smithy4s
package kinds

import smithy4s.capability._
import smithy4s.Transformation

trait PolyFunction[F[_], G[_]]{ self =>
def apply[A0](fa: F[A0]): G[A0]
Expand Down
120 changes: 120 additions & 0 deletions modules/core/src/smithy4s/Transformation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2021-2022 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package smithy4s

import kinds._

/**
* Heterogenous function construct, allows to abstract over various kinds of functions
* whilst providing an homogenous user experience without the user having to
* manually lift functions from one kind to the other.
*
*{{{
* // assuming Foo is a code-generated interface
* val fooOption : Foo[Option] = ???
* val toList = new smithy4s.PolyFunction[Option, List]{def apply[A](fa: Option[A]): List[A] = fa.toList}
* val fooList : Foo[List] = foo.transform(toList)
*}}}
*
* It is possible to plug arbitrary transformations to mechanism, such as `cats.arrow.FunctionK`
*/
trait Transformation[Func, Input, Output] {
def apply(f: Func, input: Input): Output
}

object Transformation {

/**
* A transformation that turns a monofunctor algebra into a bifunctor algebra by lifting the known errors in the
* returned types of the operations of the algebra.
*/
trait SurfaceError[F[_], G[_, _]] {
def apply[E, A](fa: F[A], projectError: Throwable => Option[E]): G[E, A]
}

/**
* A transformation that turns a bifunctor algebra into a monofunctor algebra by absorbing known errors in a
* generic error channel that handles throwables.
*/
trait AbsorbError[F[_, _], G[_]] {
def apply[E, A](fa: F[E, A], injectError: E => Throwable): G[A]
}

/**
* Partially applied transformation, can be used to create methods/extensions that allow for a reasonable UX.
*/
class PartiallyApplied[Input](input: Input) {
def apply[Func, Output](func: Func)(implicit
transform: Transformation[Func, Input, Output]
) = transform(func, input)
}

// format: off
implicit def functorK5_poly1_transformation[Alg[_[_, _, _, _, _]]: FunctorK5, F[_], G[_]]: Transformation[PolyFunction[F, G], FunctorAlgebra[Alg, F], FunctorAlgebra[Alg, G]] =
new Transformation[PolyFunction[F, G], FunctorAlgebra[Alg, F], FunctorAlgebra[Alg, G]]{
def apply(func: PolyFunction[F, G], algF: FunctorAlgebra[Alg, F]) : FunctorAlgebra[Alg, G] = FunctorK5[Alg].mapK5[Kind1[F]#toKind5, Kind1[G]#toKind5](algF, toPolyFunction5(func))
}

implicit def functorK5_poly2_transformation[Alg[_[_, _, _, _, _]]: FunctorK5, F[_,_], G[_, _]]: Transformation[PolyFunction2[F, G], BiFunctorAlgebra[Alg, F], BiFunctorAlgebra[Alg, G]] =
new Transformation[PolyFunction2[F, G], BiFunctorAlgebra[Alg, F], BiFunctorAlgebra[Alg, G]]{
def apply(func: PolyFunction2[F, G], algF: BiFunctorAlgebra[Alg, F]) : BiFunctorAlgebra[Alg, G] = FunctorK5[Alg].mapK5[Kind2[F]#toKind5, Kind2[G]#toKind5](algF, toPolyFunction5(func))
}




implicit def service_surfaceError_transformation[Alg[_[_, _, _, _, _]], F[_], G[_, _]](implicit service: Service[Alg]): Transformation[SurfaceError[F, G], FunctorAlgebra[Alg, F], BiFunctorAlgebra[Alg, G]] =
new Transformation[SurfaceError[F, G], FunctorAlgebra[Alg, F], BiFunctorAlgebra[Alg, G]]{

def apply(func: SurfaceError[F, G], algF: FunctorAlgebra[Alg, F]) : BiFunctorAlgebra[Alg, G] = {
val polyFunction = service.toPolyFunction[Kind1[F]#toKind5](algF)
val interpreter = new PolyFunction5[service.Operation, Kind2[G]#toKind5]{
def apply[I, E, O, SI, SO](op: service.Operation[I, E, O, SI, SO]): G[E,O] = {
val endpoint = service.opToEndpoint(op)
val catcher : Throwable => Option[E] = endpoint.errorable match {
case None => PartialFunction.empty[Throwable, Option[E]]
case Some(value) => value.liftError(_)
}
func.apply(polyFunction(op), catcher)
}
}
service.fromPolyFunction[Kind2[G]#toKind5](interpreter)
}
}

implicit def service_absorbError_transformation[Alg[_[_, _, _, _, _]], F[_, _], G[_]](implicit service: Service[Alg]): Transformation[AbsorbError[F, G], BiFunctorAlgebra[Alg, F], FunctorAlgebra[Alg, G]] =
new Transformation[AbsorbError[F, G], BiFunctorAlgebra[Alg, F], FunctorAlgebra[Alg, G]]{

def apply(func: AbsorbError[F, G], algF: BiFunctorAlgebra[Alg, F]) : FunctorAlgebra[Alg, G] = {
val polyFunction = service.toPolyFunction[Kind2[F]#toKind5](algF)
val interpreter = new PolyFunction5[service.Operation, Kind1[G]#toKind5]{
def apply[I, E, O, SI, SO](op: service.Operation[I, E, O, SI, SO]): G[O] = {
val endpoint = service.opToEndpoint(op)
val thrower: E => Throwable = endpoint.errorable match {
case None =>
// This case should not happen, as an endpoint without an errorable means the operation's error type is `Nothing`
_ => new RuntimeException("Error coercion problem")
case Some(value) => value.unliftError(_)
}
func.apply(polyFunction(op), thrower)
}
}
service.fromPolyFunction[Kind1[G]#toKind5](interpreter)
}
}

}
Loading