diff --git a/README.md b/README.md index a353480..1d815a5 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,11 @@ You'll typically need the following imports to use the derivation : ```scala import smithy4s.* import smithy4s.deriving.{given, *} +import smithy4s.deriving.aliases.* // for syntactically pleasant annotations +import scala.annotation.experimental // the derivation of API uses experimental metaprogramming features, at this time. + import smithy.api.* // if you want to use hints from the official smithy standard library import alloy.* // if you want to use hints from the alloy library -import scala.annotation.experimental // the derivation of API uses experimental metaprogramming features, at this time. ``` @@ -281,6 +283,21 @@ operation hello { * Defaults are supported * Scaladoc is converted to `@documentation` hints +## More concise annotations + +To reduce the verbosity induced by the `hints` annotation, it is possible to define custom annotations that create the responsibility of +creating hints, as such : + +```scala +import smithy4s.Hints +import smithy4s.deriving.HintsProvider + +case class httpGet(uri: String, status: Int) extends HintsProvider { + def hints = Hints(Http(NonEmptyString("GET"), NonEmptyString(uri), status)) +} +``` + +A few of these more-concise annotations are provided out-of-the-box in the `smithy4s.deriving.aliases` package. ## Difference between smithy4s.deriving.API and and smithy4s.Service diff --git a/modules/core/shared/src/main/scala/smithy4s/deriving/aliases/aliases.scala b/modules/core/shared/src/main/scala/smithy4s/deriving/aliases/aliases.scala new file mode 100644 index 0000000..5d5c213 --- /dev/null +++ b/modules/core/shared/src/main/scala/smithy4s/deriving/aliases/aliases.scala @@ -0,0 +1,77 @@ +package smithy4s.deriving.aliases + +import smithy4s.Hints +import smithy4s.deriving.HintsProvider +import smithy.api.* +import alloy.Discriminated +import alloy.Untagged +import alloy.SimpleRestJson +import alloy.proto.ProtoIndex + +case class httpGet(uri: String, code: Int = 200) extends HintsProvider { + def hints = Hints(Http(NonEmptyString("GET"), NonEmptyString(uri), code)) +} + +case class httpPost(uri: String, code: Int = 200) extends HintsProvider { + def hints = Hints(Http(NonEmptyString("POST"), NonEmptyString(uri), code)) +} + +case class httpPatch(uri: String, code: Int = 200) extends HintsProvider { + def hints = Hints(Http(NonEmptyString("PATCH"), NonEmptyString(uri), code)) +} + +case class httpDelete(uri: String, code: Int = 200) extends HintsProvider { + def hints = Hints(Http(NonEmptyString("DELETE"), NonEmptyString(uri), code)) +} + +case class httpPut(uri: String, code: Int = 200) extends HintsProvider { + def hints = Hints(Http(NonEmptyString("PUT"), NonEmptyString(uri), code)) +} + +case class httpLabel() extends HintsProvider { + def hints = Hints(HttpLabel()) +} + +case class httpQuery(name: String) extends HintsProvider { + def hints = Hints(HttpQuery(name)) +} + +case class httpHeader(name: String) extends HintsProvider { + def hints = Hints(HttpHeader(name)) +} + +case class httpPayload() extends HintsProvider { + def hints = Hints(HttpPayload()) +} + +case class httpError(code: Int) extends HintsProvider { + def hints = Hints(HttpError(code)) +} + +case class readonly() extends HintsProvider { + def hints = Hints(Readonly()) +} + +case class idempotent() extends HintsProvider { + def hints = Hints(Idempotent()) +} + +case class jsonName(name: String) extends HintsProvider { + def hints = Hints(JsonName(name)) +} + +case class simpleRestJson() extends HintsProvider { + def hints = Hints(SimpleRestJson()) +} + +case class discriminated(fieldName: String) extends HintsProvider { + def hints = Hints(Discriminated(fieldName)) +} + +case class untagged() extends HintsProvider { + def hints = Hints(Untagged()) +} + +case class protoIndex(value: Int) extends HintsProvider { + def hints = Hints(ProtoIndex(value)) +} diff --git a/modules/core/shared/src/main/scala/smithy4s/deriving/annotations.scala b/modules/core/shared/src/main/scala/smithy4s/deriving/annotations.scala index 66ed63b..025b447 100644 --- a/modules/core/shared/src/main/scala/smithy4s/deriving/annotations.scala +++ b/modules/core/shared/src/main/scala/smithy4s/deriving/annotations.scala @@ -22,6 +22,17 @@ import smithy4s.{Hint, Hints} abstract class HintsProvider extends MetaAnnotation { def hints: Hints } + +/** + * Allows to capture any value the type of which has a `smithy4s.ShapeTag` instance, as a hint. + * The values are typically generated by `Smithy4s` from smithy shapes. + * + * See https://smithy.io/2.0/trait-index.html for the official list of traits. Smithy4s bundled generated data-types + * for each of them. + * + * See https://github.com/disneystreaming/alloy/ for the companion library of smithy traits that Smithy4s also bundles + * generated datatypes for. + */ case class hints(first: Hint, others: Hint*) extends HintsProvider { def hints: Hints = Hints.fromSeq(first +: others) } diff --git a/modules/core/shared/src/main/scala/smithy4s/deriving/internals/macros.scala b/modules/core/shared/src/main/scala/smithy4s/deriving/internals/macros.scala index 0ca82d6..43ddde6 100644 --- a/modules/core/shared/src/main/scala/smithy4s/deriving/internals/macros.scala +++ b/modules/core/shared/src/main/scala/smithy4s/deriving/internals/macros.scala @@ -309,10 +309,9 @@ private def altsExpression[T: Type]( private def operationHints(annotations: List[Expr[Any]])(using Quotes): Expr[Hints] = annotations - .collectFirst { case '{ $smithyAnnotation: HintsProvider } => - '{ $smithyAnnotation.hints } - } - .getOrElse('{ Hints.empty }) + .collect { case '{ $smithyAnnotation: HintsProvider } => '{ $smithyAnnotation.hints }} + .fold('{Hints.empty}){ case (left, right) => '{$left ++ $right}} + private def paramHintsMap(using Quotes) (annotations: List[List[Expr[Any]]], labels: List[String], paramDocs: Map[String, String], paramSymbols: List[quotes.reflect.Symbol]): Map[String, Expr[Hints]] = { diff --git a/modules/examples/shared/src/main/scala/model.scala b/modules/examples/shared/src/main/scala/model.scala index 37c4cc2..40604cd 100644 --- a/modules/examples/shared/src/main/scala/model.scala +++ b/modules/examples/shared/src/main/scala/model.scala @@ -18,30 +18,25 @@ package examples import smithy4s.* import smithy4s.deriving.{given, *} -import smithy.api.* -import alloy.* +import smithy4s.deriving.aliases.* import cats.effect.IO import scala.annotation.experimental -// Just making it simpler to construct hints -given [A, B](using Bijection[A, B]): Conversion[A, B] with { - def apply(a: A): B = summon[Bijection[A, B]](a) -} - -@hints(HttpError(503)) +@httpError(503) case class LocationNotRecognised(errorMessage: String) extends Throwable derives Schema { override def getMessage(): String = errorMessage } @experimental -@hints(SimpleRestJson()) +@simpleRestJson class HelloWorldService() derives API { @errors[LocationNotRecognised] - @hints(Http(method = "GET", uri = "/hello/{name}"), Readonly()) + @readonly + @httpGet("/hello/{name}") def hello( - @hints(HttpLabel()) name: String, - @hints(HttpQuery("from")) from: Option[String] + @httpLabel() name: String, + @httpQuery("from") from: Option[String] ): IO[String] = from match { case None => IO(s"Hello $name!") case Some(loc) if loc.isEmpty() => IO.raiseError(LocationNotRecognised("Empty location"))