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

Adds Handlers constructs and functions #1522

Merged
merged 7 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ We apologize for the inconvenience.
* `smithy4sUpdateLSPConfig`: Replace `imports` with `sources` to be more in line with idiomatic smithy-build config in https://github.com/disneystreaming/smithy4s/pull/1518 (see https://github.com/disneystreaming/smithy4s/issues/1459)
* Update smithy: 1.45.0 to 1.49.0 (binary breaking) in https://github.com/disneystreaming/smithy4s/pull/1485
* Rendered type aliases are now sorted alphabetically
* Adds handlers construct to facilitate the decoupling of operation implementations

# 0.18.18

Expand Down
66 changes: 66 additions & 0 deletions modules/bootstrapped/test/src/smithy4s/HandlerSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package smithy4s.other

import munit.FunSuite
import smithy4s.example.KVStoreOperation
import cats.Id
import java.util.concurrent.atomic.AtomicReference
import smithy4s.example.Value
import smithy4s.example.KVStore
import smithy4s.example.Key

class HandlerSpec extends FunSuite {

def get(ref: AtomicReference[Map[String, String]]) =
KVStoreOperation.Get.handler[Id] { input =>
Value(ref.get().get(input.key).getOrElse("unknown"))
}

def put(ref: AtomicReference[Map[String, String]]) =
KVStoreOperation.Put.handler[Id] { input =>
val _ = ref.updateAndGet(_ + (input.key -> input.value))
}

// Handlers can also be created by inheriting from a class.
case class delete(ref: AtomicReference[Map[String, String]])
extends KVStoreOperation.Delete.Handler[Id] {
def run(input: Key): Id[Unit] = {
val _ = ref.updateAndGet(_ - input.key)
}
}

test("Handler composition: orElse ") {

val ref = new AtomicReference(Map.empty[String, String])
val kvStore: KVStore[Id] = get(ref)
.orElse(put(ref))
.orElse(delete(ref))
.asService(KVStore)
.throwing

kvStore.put("a", "b")
val b = kvStore.get("a").value
kvStore.delete("a")

assertEquals(b, "b")
assertEquals(ref.get(), Map.empty[String, String])
}

test("Handler composition: combineAll ") {
val ref = new AtomicReference(Map.empty[String, String])
val kvStore: KVStore[Id] = KVStore
.fromFunctorHandlers[Id](
get(ref),
put(ref),
delete(ref)
)
.throwing

kvStore.put("a", "b")
val b = kvStore.get("a").value
kvStore.delete("a")

assertEquals(b, "b")
assertEquals(ref.get(), Map.empty[String, String])
}

}
3 changes: 3 additions & 0 deletions modules/core/src-2/kinds/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,18 @@ package object kinds {
type Kind1[F[_]] = {
type toKind2[E, O] = F[O]
type toKind5[I, E, O, SI, SO] = F[O]
type optional5[I, E, O, SI, SO] = Option[F[O]]
type handler[I, E, O, SI, SO] = I => F[O]
}

type Kind2[F[_, _]] = {
type toKind5[I, E, O, SI, SO] = F[E, O]
type optional5[I, E, O, SI, SO] = Option[F[E, O]]
type handler[I, E, O, SI, SO] = I => F[E, O]
}

type Kind5[F[_, _, _, _, _]] = {
type optional[I, E, O, SI, SO] = Option[F[I, E, O, SI, SO]]
type handler[I, E, O, SI, SO] = I => F[I, E, O, SI, SO]
}
}
3 changes: 3 additions & 0 deletions modules/core/src-3/kinds/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,18 @@ package object kinds {
type Kind1[F[_]] = {
type toKind2[E, O] = F[O]
type toKind5[I, E, O, SI, SO] = F[O]
type optional5[I, E, O, SI, SO] = Option[F[O]]
type handler[I, E, O, SI, SO] = I => F[O]
}

type Kind2[F[_, _]] = {
type toKind5[I, E, O, SI, SO] = F[E, O]
type optional5[I, E, O, SI, SO] = Option[F[E, O]]
type handler[I, E, O, SI, SO] = I => F[E, O]
}

type Kind5[F[_, _, _, _, _]] = {
type optional[I, E, O, SI, SO] = Option[F[I, E, O, SI, SO]]
type handler[I, E, O, SI, SO] = I => F[I, E, O, SI, SO]
}

Expand Down
49 changes: 48 additions & 1 deletion modules/core/src/smithy4s/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package smithy4s

import smithy4s.schema._
import schema.ErrorSchema
import smithy4s.kinds.PolyFunction5
import smithy4s.kinds._

/**
* A representation of a smithy operation.
Expand All @@ -43,7 +45,7 @@ import schema.ErrorSchema
* be encoded a great many ways, using a great many libraries)
*/
// scalafmt: {maxColumn = 120}
trait Endpoint[Op[_, _, _, _, _], I, E, O, SI, SO] {
trait Endpoint[Op[_, _, _, _, _], I, E, O, SI, SO] { self =>

final def mapSchema(
f: OperationSchema[I, E, O, SI, SO] => OperationSchema[I, E, O, SI, SO]
Expand All @@ -69,6 +71,51 @@ trait Endpoint[Op[_, _, _, _, _], I, E, O, SI, SO] {
err.liftError(throwable).map(err -> _)
}
}

/**
* Allows the creation of a handler via lifting a function that returns some functor.
*/
def handler[F[_]](f: I => F[O]): EndpointHandler[Op, Kind1[F]#toKind5] = new Handler[F] {
def run(input: I): F[O] = f(input)
}

/**
* Allows the creation of a handler via lifting a function that returns some bi-functor.
*/
def errorAwareHandler[F[_, _]](f: I => F[E, O]): EndpointHandler[Op, Kind2[F]#toKind5] = new ErrorAwareHandler[F] {
def run(input: I): F[E, O] = f(input)
}

/**
* Allows the creation of a hander via object-oriented inheritance.
*/
abstract class Handler[F[_]] extends EndpointHandler[Op, Kind1[F]#toKind5] {
def run(input: I): F[O]
protected[smithy4s] def lift[Alg[_[_, _, _, _, _]]](
service: Service.Aux[Alg, Op]
): PolyFunction5[Op, Kind1[F]#optional5] = new PolyFunction5[Op, Kind1[F]#optional5] {
val ord = service.endpoints.indexOf(self)

def apply[I_, E_, O_, SI_, SO_](op: Op[I_, E_, O_, SI_, SO_]): Option[F[O_]] = if (service.ordinal(op) == ord) {
Some(run(service.input(op).asInstanceOf[I]).asInstanceOf[F[O_]])
} else None
}
}

abstract class ErrorAwareHandler[F[_, _]] extends EndpointHandler[Op, Kind2[F]#toKind5] {
def run(input: I): F[E, O]
protected[smithy4s] def lift[Alg[_[_, _, _, _, _]]](
service: Service.Aux[Alg, Op]
): PolyFunction5[Op, Kind2[F]#optional5] = new PolyFunction5[Op, Kind2[F]#optional5] {
val ord = service.endpoints.indexOf(self)

def apply[I_, E_, O_, SI_, SO_](op: Op[I_, E_, O_, SI_, SO_]): Option[F[E_, O_]] = if (
service.ordinal(op) == ord
) {
Some(run(service.input(op).asInstanceOf[I]).asInstanceOf[F[E_, O_]])
} else None
}
}
}

object Endpoint {
Expand Down
135 changes: 135 additions & 0 deletions modules/core/src/smithy4s/EndpointHandler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2021-2024 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 smithy4s.kinds.PolyFunction5
import smithy4s.kinds.Kind5
import smithy4s.EndpointHandler.AsService

/**
* Composable handler that allows to implement a specific endpoint in isolation.
*
* Handlers are composable and can be reconciled into the service the operations belong to.
*/
trait EndpointHandler[Op[_, _, _, _, _], F[_, _, _, _, _]] {
import EndpointHandler.Combined

protected[smithy4s] def lift[Alg[_[_, _, _, _, _]]](
service: Service.Aux[Alg, Op]
): PolyFunction5[Op, Kind5[F]#optional]

final def asService[Alg[_[_, _, _, _, _]]](
service: Service.Aux[Alg, Op]
): AsService[Alg, F] =
new EndpointHandler.AsServiceImpl[Alg, Op, F](this, service)

final def orElse(other: EndpointHandler[Op, F]): EndpointHandler[Op, F] =
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: orElse is used in cats/CE for error handling and could be confusing, perhaps we can find a name that isn't, e.g. just or?

other ideas off the top of my head: merge, add, combine

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(this, other) match {
case (Combined(left), Combined(right)) => Combined(left ++ right)
case (other, Combined(right)) => Combined(other +: right)
case (Combined(left), other) => Combined(left :+ other)
case (left, right) => Combined(Vector(left, right))
}
}

// scalafmt: { maxColumn = 120 }
object EndpointHandler {

/**
* Partial step when handlers are transformed into a service, allowing them to decide how to handle
* un-implemented endpoints.
*/
trait AsService[Alg[_[_, _, _, _, _]], F[_, _, _, _, _]] {

/**
* Returns an instance of the algebra that throws when one of the methods doesn't have a matching endpoint
* handler
*/
def throwing: Alg[F]

/**
* Returns an instance of the algebra that raises an error in an effect when one of the methods doesn't have a matching
* endpoint handler
*/
def failingWith(f: ShapeId => F[Any, Nothing, Nothing, Nothing, Nothing]): Alg[F]

/**
* Returns an instance of the algebra that wraps implemented methods in `Some` and return `None` on unimplemented methods
*/
def partial: Alg[Kind5[F]#optional]
}

private class AsServiceImpl[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_, _, _, _, _]](
handler: EndpointHandler[Op, F],
service: Service.Aux[Alg, Op]
) extends AsService[Alg, F] {
def throwing: Alg[F] = {
val lifted = handler.lift(service)
val interpreter = new PolyFunction5[Op, F] {
def apply[I, E, O, SI, SO](op: Op[I, E, O, SI, SO]) =
lifted(op).getOrElse {
val endpointId = service.endpoint(op).id
throw new NotImplementedError(endpointId.show)
}
}
service.fromPolyFunction(interpreter)
}

def failingWith(f: ShapeId => F[Any, Nothing, Nothing, Nothing, Nothing]): Alg[F] = {
val lifted = handler.lift(service)
val interpreter = new PolyFunction5[Op, F] {
def apply[I, E, O, SI, SO](op: Op[I, E, O, SI, SO]) =
lifted(op).getOrElse {
val endpointId = service.endpoint(op).id
f(endpointId).asInstanceOf[F[I, E, O, SI, SO]]
}
}
service.fromPolyFunction(interpreter)
}

def partial: Alg[Kind5[F]#optional] =
service.fromPolyFunction[Kind5[F]#optional](handler.lift(service))
}

private[smithy4s] def combineAll[Op[_, _, _, _, _], F[_, _, _, _, _]](
handlers: EndpointHandler[Op, F]*
): EndpointHandler[Op, F] =
Combined(handlers.toVector)
Baccata marked this conversation as resolved.
Show resolved Hide resolved

private case class Combined[Op[_, _, _, _, _], F[_, _, _, _, _]](
handlers: Vector[EndpointHandler[Op, F]]
) extends EndpointHandler[Op, F] {
protected[smithy4s] def lift[Alg[_[_, _, _, _, _]]](
service: Service.Aux[Alg, Op]
): PolyFunction5[Op, Kind5[F]#optional] =
new PolyFunction5[Op, Kind5[F]#optional] {
val lifted = handlers.map(_.lift(service))

def apply[I, E, O, SI, SO](
op: Op[I, E, O, SI, SO]
): Option[F[I, E, O, SI, SO]] = {
var result: Option[F[I, E, O, SI, SO]] = None
var i = 0
while (result == None && i < lifted.size) {
result = lifted(i)(op)
i += 1
}
result
}
}
}
}
19 changes: 18 additions & 1 deletion modules/core/src/smithy4s/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,27 @@ trait Service[Alg[_[_, _, _, _, _]]] extends FunctorK5[Alg] with HasId {
final def impl[F[_]](compiler: FunctorEndpointCompiler[F]) : Impl[F] = algebra[Kind1[F]#toKind5](compiler)

/**
* A monofunctor-specialised version of [[algebra]]
* A bifunctor-specialised version of [[algebra]]
*/
final def errorAware[F[_, _]](compiler: BiFunctorEndpointCompiler[F]) : ErrorAware[F] = algebra[Kind2[F]#toKind5](compiler)

/**
* Allows to turn a list of endpoint handlers into an instance of [[Alg]].
*/
final def fromHandlers[F[_, _, _, _, _]](handlers: EndpointHandler[Operation, F]*): EndpointHandler.AsService[Alg, F] =
EndpointHandler.combineAll(handlers:_*).asService(this)

/**
* A functor-specialised version of [[fromHandlers]], to help scala 2.12
*/
final def fromFunctorHandlers[F[_]](handlers: EndpointHandler[Operation, Kind1[F]#toKind5]*) : EndpointHandler.AsService[Alg, Kind1[F]#toKind5]
= fromHandlers[Kind1[F]#toKind5](handlers:_*)

/**
* A bifunctor-specialised version of [[fromHandlers]], to help scala 2.12
*/
final def fromBifunctorHandlers[F[_, _]](handlers: EndpointHandler[Operation, Kind2[F]#toKind5]*) : EndpointHandler.AsService[Alg, Kind2[F]#toKind5]
= fromHandlers[Kind2[F]#toKind5](handlers:_*)
}

object Service {
Expand Down
Loading
Loading