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 service transformation utils #1644

Merged
merged 3 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ When adding entries, please treat them as if they could end up in a release any

Thank you!

# 0.18.30

* Add utilities for Service.Builder in [#1644](https://github.com/disneystreaming/smithy4s/pull/1644)

# 0.18.29

* Fix for decoding of required nullable fields and some combinations of refinements with nullable fields (see [#1637](https://github.com/disneystreaming/smithy4s/pull/1637))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ import munit._
import smithy4s.kinds.PolyFunction5
import smithy4s.example.FooServiceGen
import smithy.api.Documentation
import smithy4s.schema.OperationSchema
import smithy4s.Schema
import smithy4s.kinds.PolyFunction

class ServiceBuilderSpec extends FunSuite {

val service = smithy4s.example.FooService

val builder = smithy4s.Service.Builder.fromService(service)
val builder = service.toBuilder

test(
"can replace the following values (Id, Version and Hints) using withId, withVersion, withHints"
Expand Down Expand Up @@ -91,4 +94,30 @@ class ServiceBuilderSpec extends FunSuite {
)
}

test("can easily transform endpoint's inputs using schema PolyFunctions") {

val fk = new PolyFunction[Schema, Schema] {
def apply[A](schema: Schema[A]): Schema[A] =
schema.withId(schema.shapeId.withNamespace("replaced"))
}

val result = builder
.mapEndpointEach(
Endpoint.mapSchema(
OperationSchema
.mapInputK(fk)
.andThen(
OperationSchema.mapOutputK(fk)
)
)
)
.build

val namespaces =
result.endpoints.map(_.input.shapeId.namespace).toSet ++
result.endpoints.map(_.output.shapeId.namespace).toSet

assert(namespaces == Set("replaced"))
}

}
7 changes: 7 additions & 0 deletions modules/core/src/smithy4s/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ trait Endpoint[Op[_, _, _, _, _], I, E, O, SI, SO] { self =>

object Endpoint {

def mapSchema[Op[_, _, _, _, _]](
f: PolyFunction5[OperationSchema, OperationSchema]
): PolyFunction5[Endpoint[Op, *, *, *, *, *], Endpoint[Op, *, *, *, *, *]] =
new PolyFunction5[Endpoint[Op, *, *, *, *, *], Endpoint[Op, *, *, *, *, *]] {
def apply[I, E, O, SI, SO](fa: Endpoint[Op, I, E, O, SI, SO]): Endpoint[Op, I, E, O, SI, SO] = fa.mapSchema(f(_))
}

trait Middleware[A] { self =>
def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(endpoint: service.Endpoint[_, _, _, _, _]): A => A
final def biject[B](to: A => B)(from: B => A): Middleware[B] = new Middleware[B] {
Expand Down
2 changes: 2 additions & 0 deletions modules/core/src/smithy4s/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ trait Service[Alg[_[_, _, _, _, _]]] extends FunctorK5[Alg] with HasId {
*/
final def fromBifunctorHandlers[F[_, _]](handlers: EndpointHandler[Operation, Kind2[F]#toKind5]*) : FromHandlers[Kind2[F]#toKind5]
= fromHandlers[Kind2[F]#toKind5](handlers:_*)

final def toBuilder: Service.Builder[Alg, Operation] = Service.Builder.fromService(this)
}

object Service {
Expand Down
4 changes: 4 additions & 0 deletions modules/core/src/smithy4s/ShapeId.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import smithy.api.IdRef

final case class ShapeId(namespace: String, name: String) extends HasId {
def show = s"$namespace#$name"

def withNamespace(namespace: String): ShapeId = copy(namespace = namespace)
def withName(name: String): ShapeId = copy(name = name)
def withMember(member: String): ShapeId.Member = ShapeId.Member(this, member)

override def toString = show
override def id: ShapeId = this
}
Expand Down
54 changes: 54 additions & 0 deletions modules/core/src/smithy4s/schema/OperationSchema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package smithy4s
package schema

import smithy4s.internals.InputOutput
import smithy4s.kinds.PolyFunction5
import smithy4s.kinds.PolyFunction

final case class OperationSchema[I, E, O, SI, SO] private[smithy4s] (
id: ShapeId,
Expand Down Expand Up @@ -96,3 +98,55 @@ final case class OperationSchema[I, E, O, SI, SO] private[smithy4s] (
copy(streamedOutput = streamedOutput.map(f))

}

object OperationSchema {
Copy link
Member Author

Choose a reason for hiding this comment

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

thought: if these are deemed overkill, I'm ok with removing them.

Copy link
Member Author

Choose a reason for hiding this comment

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

the rest are more high-value though, I'd keep them as-is

Copy link
Contributor

@Baccata Baccata Feb 6, 2025

Choose a reason for hiding this comment

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

Couldn't these be methods on the OperationSchema class instead of static functions ?

Copy link
Member Author

Choose a reason for hiding this comment

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

no, if we want them as polyfunctions (and we do, if we want nice things like what you see in the new test), they have to be outside of the class

Copy link
Contributor

Choose a reason for hiding this comment

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

fair nuff

def mapInputK(
fk: PolyFunction[Schema, Schema]
): PolyFunction5[OperationSchema, OperationSchema] =
new PolyFunction5[OperationSchema, OperationSchema] {
def apply[I, E, O, SI, SO](
op: OperationSchema[I, E, O, SI, SO]
): OperationSchema[I, E, O, SI, SO] =
op.mapInput(fk(_))
}

def mapOutputK(
fk: PolyFunction[Schema, Schema]
): PolyFunction5[OperationSchema, OperationSchema] =
new PolyFunction5[OperationSchema, OperationSchema] {
def apply[I, E, O, SI, SO](
op: OperationSchema[I, E, O, SI, SO]
): OperationSchema[I, E, O, SI, SO] =
op.mapOutput(fk(_))
}

def mapErrorK(
fk: PolyFunction[ErrorSchema, ErrorSchema]
): PolyFunction5[OperationSchema, OperationSchema] =
new PolyFunction5[OperationSchema, OperationSchema] {
def apply[I, E, O, SI, SO](
op: OperationSchema[I, E, O, SI, SO]
): OperationSchema[I, E, O, SI, SO] =
op.mapError(fk(_))
}

def mapStreamingInputK(
fk: PolyFunction[StreamingSchema, StreamingSchema]
): PolyFunction5[OperationSchema, OperationSchema] =
new PolyFunction5[OperationSchema, OperationSchema] {
def apply[I, E, O, SI, SO](
op: OperationSchema[I, E, O, SI, SO]
): OperationSchema[I, E, O, SI, SO] =
op.mapStreamedInput(fk(_))
}

def mapStreamingOutputK(
fk: PolyFunction[StreamingSchema, StreamingSchema]
): PolyFunction5[OperationSchema, OperationSchema] =
new PolyFunction5[OperationSchema, OperationSchema] {
def apply[I, E, O, SI, SO](
op: OperationSchema[I, E, O, SI, SO]
): OperationSchema[I, E, O, SI, SO] =
op.mapStreamedOutput(fk(_))
}
}
Loading