Skip to main content

Service Product

As of smithy4s version 0.18.x you can also generate a service interface in which each method doesn't receive an input. Instead, the output of each method has the usual return type, which already includes the input as a type parameter. We call this version a "service product" because it can be seen as the product of all the operations of the service.

To generate a service product, annotate the service definition with

@generateServiceProduct:

$version: "2"

namespace smithy4s.example.product

use smithy4s.meta#generateServiceProduct

@generateServiceProduct
service ExampleService {
operations: [ExampleOperation]
}

operation ExampleOperation {
input := {
@required
a: String
}
output := {
@required
b: String
}
}

This will generate the following interface:

trait ExampleServiceProductGen[F[_, _, _, _, _]] {
def exampleOperation: F[ExampleOperationInput, Nothing, ExampleOperationOutput, Nothing, Nothing]
}

and the following implementation of ServiceProduct:

object ExampleServiceProductGen extends ServiceProduct[ExampleServiceProductGen]

You will be able to access the service product version of the service like this:

import smithy4s.example.product._

ExampleServiceGen.serviceProduct
// res0: smithy4s.ServiceProduct.Aux[ExampleServiceProductGen, ExampleServiceGen] = smithy4s.example.product.ExampleServiceProductGen$@745f6bee

Or, more generically:

import smithy4s.ServiceProduct

def productOf[Alg[_[_, _, _, _, _]]](mirror: ServiceProduct.Mirror[Alg]) = mirror.serviceProduct

// example
def exampleProduct = productOf(ExampleService)

With service products, you can call service methods without providing their inputs directly.

Here are a couple ways you can use this as a library author:

Static description of services

Implementation
import smithy4s.kinds.PolyFunction5

type Describe[_, _, _, _, _] = String

def descriptor[Alg[_[_, _, _, _, _]]](mirror: ServiceProduct.Mirror[Alg]): mirror.Prod[Describe] =
mirror
.serviceProduct
.mapK5(
mirror.serviceProduct.endpointsProduct,
new PolyFunction5[mirror.serviceProduct.service.Endpoint, Describe] {

override def apply[I, E, O, SI, SO](
fa: mirror.serviceProduct.service.Endpoint[I, E, O, SI, SO]
): Describe[I, E, O, SI, SO] =
s"def ${fa.name}(input: ${fa.input.shapeId.name}): ${fa.output.shapeId.name}"

},
)
// Usage

val desc: String = descriptor(ExampleService).exampleOperation
// desc: String = "def ExampleOperation(input: ExampleOperationInput): ExampleOperationOutput"

Non-linear input of operation

Implementation
import smithy4s.ShapeId

type Id[A] = A

val impl: ExampleService[Id] =
new ExampleService[Id] {

override def exampleOperation(input: String): ExampleOperationOutput = ExampleOperationOutput(
s"Output for $input!"
)

}
// impl: ExampleService[Id] = repl.MdocSession$MdocApp$$anon$2@71d6ecf5

type ListClient[I, _, O, _, _] = List[I] => List[O]

def listClient[Alg[_[_, _, _, _, _]], Prod[_[_, _, _, _, _]]](
impl: smithy4s.kinds.FunctorAlgebra[Alg, Id]
)(
implicit sp: ServiceProduct.Aux[Prod, Alg]
): Prod[ListClient] = sp
.mapK5(
sp.endpointsProduct,
new PolyFunction5[sp.service.Endpoint, ListClient] {

private val interp = sp.service.toPolyFunction(impl)

override def apply[I, E, O, SI, SO](
fa: sp.service.Endpoint[I, E, O, SI, SO]
): List[I] => List[O] = _.map(in => interp(fa.wrap(in)))

},
)
listClient(impl)( /* implicit scope problem here - TODO */ ExampleService.serviceProduct)
.exampleOperation(
List("a", "b", "c").map(ExampleOperationInput(_))
)
// res1: List[ExampleOperationOutput] = List(
// ExampleOperationOutput(b = "Output for a!"),
// ExampleOperationOutput(b = "Output for b!"),
// ExampleOperationOutput(b = "Output for c!")
// )

Fluent service builder

Implementation
type ToList[_, _, O, _, _] = List[O]

trait EndpointHandlerBuilder[I, E, O, SI, SO] {
def apply(f: I => O): EndpointHandler
}

sealed trait EndpointHandler {
type I_
type O_
def id: ShapeId
def function: I_ => O_
}

case class PartialBuilder[Alg[_[_, _, _, _, _]], Prod[_[_, _, _, _, _]]](
mirror: ServiceProduct.Mirror.Aux[Alg, Prod],
handlers: List[EndpointHandler],
) {
private val sp: ServiceProduct.Aux[Prod, Alg] = mirror.serviceProduct

private val ehbProduct = sp
.mapK5(
sp.endpointsProduct,
new PolyFunction5[sp.service.Endpoint, EndpointHandlerBuilder] {

override def apply[I, E, O, SI, SO](
fa: sp.service.Endpoint[I, E, O, SI, SO]
): EndpointHandlerBuilder[I, E, O, SI, SO] =
new EndpointHandlerBuilder[I, E, O, SI, SO] {

override def apply(f: I => O): EndpointHandler =
new EndpointHandler {
type I_ = I
type O_ = O
override val id: ShapeId = fa.id
override val function: I_ => O_ = f
}

}

},
)

def build: Alg[ToList] = sp
.service
.algebra(new sp.service.EndpointCompiler[ToList] {

override def apply[I, E, O, SI, SO](
fa: sp.service.Endpoint[I, E, O, SI, SO]
): I => List[O] = {

val matchingHandlers = handlers
.filter(_.id == fa.id)
// A bit of type unsafety, to simplify things
.map(_.function.asInstanceOf[I => O])

i => matchingHandlers.map(_.apply(i))

}

})

def withHandler(
op: Prod[EndpointHandlerBuilder] => EndpointHandler
): PartialBuilder[Alg, Prod] = copy(handlers = handlers :+ op(ehbProduct))

}

def partialBuilder[Alg[_[_, _, _, _, _]]](
mirror: ServiceProduct.Mirror[Alg]
): PartialBuilder[Alg, mirror.Prod] = new PartialBuilder[Alg, mirror.Prod](mirror, handlers = Nil)
val listService: ExampleServiceGen[ToList] =
partialBuilder(ExampleService)
.withHandler(_.exampleOperation { (in: ExampleOperationInput) =>
ExampleOperationOutput(s"First output for ${in.a}!")
})
.withHandler(_.exampleOperation { (in: ExampleOperationInput) =>
ExampleOperationOutput(s"Another output for ${in.a}!")
})
.build
// listService: ExampleServiceGen[ToList] = smithy4s.example.product.ExampleServiceOperation$Transformed@7840d9bd

listService.exampleOperation("hello")
// res2: ToList[ExampleOperationInput, Nothing, ExampleOperationOutput, Nothing, Nothing] = List(
// ExampleOperationOutput(b = "First output for hello!"),
// ExampleOperationOutput(b = "Another output for hello!")
// )