Skip to content

Commit

Permalink
Derive http endpoints from hints (#41)
Browse files Browse the repository at this point in the history
* Derive HttpEndpoint from Hint instead of relying on subtyping

This derives the HttpEndpoint instance associated to an Endpoint, by
searching smithy.api.Http in the the Endpoint's hints, and compiling
the found data to the correct interface, by means of Schematic.

The rationale is the following :
1. allow for dynamically instantiated service to have HttpService
without any bespoke processing.
2. open the door for splitting the http package away from the core
module

Also : 

* AlsRemove uri-specific methods from core package object
* Fix URIEncoderDecoder to actually do uri encoding 

Co-authored-by: Jakub Kozłowski <[email protected]>
  • Loading branch information
Baccata and kubukoz authored Jan 14, 2022
1 parent 05ab381 commit 56843ea
Show file tree
Hide file tree
Showing 19 changed files with 418 additions and 192 deletions.
1 change: 0 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,6 @@ lazy val `aws-http4s` = projectMatrix
(ThisBuild / baseDirectory).value / "sampleSpecs" / "dynamodb.2012-08-10.json"
)
)
.settings(Smithy4sPlugin.doNotPublishArtifact)
.jvmPlatform(latest2ScalaVersions, jvmDimSettings)
.jsPlatform(latest2ScalaVersions, jsDimSettings)

Expand Down
2 changes: 1 addition & 1 deletion modules/aws-kernel/src/smithy4s/aws/AwsSignature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package smithy4s.aws.kernel

import smithy4s.http.internals.URIEncoderDecoder.{encodeOthers => uriEncode}
import smithy4s.http.internals.URIEncoderDecoder.{encode => uriEncode}
import smithy4s.http.CaseInsensitive
import smithy4s.http.HttpMethod

Expand Down
47 changes: 2 additions & 45 deletions modules/codegen/src/smithy4s/codegen/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,6 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>

val opName = op.name
val traitName = s"${serviceName}Operation"
val inputName = op.input.render
val input =
if (op.input == Type.unit) "" else "input"
val errorName = if (op.errors.isEmpty) "Nothing" else s"${op.name}Error"
Expand All @@ -254,13 +253,6 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
s" with $Errorable_[$errorName]"
} else ""

val httpEndpoint =
op.hints
.collectFirst { case Hint.Http(_, _, _) =>
s" with http.HttpEndpoint[$inputName]"
}
.getOrElse("")

val errorUnion: Option[Union] = for {
errorNel <- NonEmptyList.fromList(op.errors)
alts <- errorNel.traverse { t =>
Expand All @@ -277,8 +269,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
s"case class $opName($params) extends $traitName[${op.renderAlgParams}]",
obj(
opName,
ext =
s"$Endpoint_[${traitName}, ${op.renderAlgParams}]$httpEndpoint$errorable"
ext = s"$Endpoint_[${traitName}, ${op.renderAlgParams}]$errorable"
)(
renderId(op.name, op.originalNamespace),
s"val input: $Schema_[${op.input.render}] = ${op.input.schemaRef}.withHints(smithy4s.internals.InputOutput.Input)",
Expand All @@ -287,8 +278,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
renderStreamingSchemaVal("streamedOutput", op.streamedOutput),
renderHintsValWithId(op.hints),
s"def wrap(input: ${op.input.render}) = ${opName}($input)",
renderErrorable(op),
renderHttpSpecific(op)
renderErrorable(op)
),
renderedErrorUnion
).addImports(op.imports).addImports(syntaxImport)
Expand Down Expand Up @@ -374,39 +364,6 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
).addImports(imports)
}

private def renderHttpSpecific(op: Operation): RenderResult = lines {
op.hints
.collectFirst { case Hint.Http(method, uriPattern, code) =>
val segments = uriPattern
.map {
case Segment.Label(value) =>
s"""http.PathSegment.label("$value")"""
case Segment.GreedyLabel(value) =>
s"""http.PathSegment.greedy("$value")"""
case Segment.Static(value) =>
s"""http.PathSegment.static("$value")"""
}
.mkString("List(", ", ", ")")

val uriValue = uriPattern
.map {
case Segment.Label(value) =>
s"$${smithy4s.segment(input.$value)}"
case Segment.GreedyLabel(value) =>
s"$${smithy4s.greedySegment(input.$value)}"
case Segment.Static(value) => value
}
.mkString("s\"", "/", "\"")
lines(
s"def path(input: ${op.input.render}) = $uriValue",
s"val path = $segments",
s"val code: Int = $code",
s"val method: http.HttpMethod = http.HttpMethod.$method"
).addImports(Set("smithy4s.http"))
}
.getOrElse(empty)
}

private def renderErrorable(op: Operation): RenderResult = {
val errorName = op.name + "Error"

Expand Down
28 changes: 25 additions & 3 deletions modules/core/src/smithy4s/http/HttpEndpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package smithy4s
package http

import smithy4s.syntax._
import smithy.api.Http

trait HttpEndpoint[I] {
def path(input: I): String
def path: List[PathSegment]
Expand All @@ -36,8 +39,27 @@ object HttpEndpoint {

def cast[Op[_, _, _, _, _], I, E, O, SI, SO](
endpoint: Endpoint[Op, I, E, O, SI, SO]
): Option[HttpEndpoint[I]] = endpoint match {
case he: HttpEndpoint[_] => Some(he.asInstanceOf[HttpEndpoint[I]])
case _ => None
): Option[HttpEndpoint[I]] = {
for {
http <- endpoint.hints.get(Http)
httpMethod <- HttpMethod.fromString(http.method.value)
httpPath <- internals.pathSegments(http.uri.value)
encoder <- endpoint.input
.withHints(http)
.compile(internals.SchematicPathEncoder)
.get
} yield {
new HttpEndpoint[I] {
def path(input: I): String = {
val sb = new StringBuilder()
encoder.encode(sb, input)
sb.result()
}
val path: List[PathSegment] = httpPath.toList
val method: HttpMethod = httpMethod
val code: Int = http.code.getOrElse(200)
}
}
}

}
6 changes: 6 additions & 0 deletions modules/core/src/smithy4s/http/Method.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,10 @@ object HttpMethod {
case object DELETE extends HttpMethod
case object GET extends HttpMethod
case object PATCH extends HttpMethod

val values = List(PUT, POST, DELETE, GET, PATCH)

def fromString(s: String): Option[HttpMethod] = values.find { m =>
CaseInsensitive(s) == CaseInsensitive(m.showCapitalised)
}
}
1 change: 1 addition & 0 deletions modules/core/src/smithy4s/http/PathSegment.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ object PathSegment {
case class StaticSegment(value: String) extends PathSegment
case class LabelSegment(value: String) extends PathSegment
case class GreedySegment(value: String) extends PathSegment

}
64 changes: 64 additions & 0 deletions modules/core/src/smithy4s/http/internals/PathEncode.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2021 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.http.internals

import smithy4s.internals.Hinted

trait PathEncode[A] { self =>
def encode(sb: StringBuilder, a: A): Unit
def encodeGreedy(sb: StringBuilder, a: A): Unit

def contramap[B](from: B => A): PathEncode[B] = new PathEncode[B] {
def encode(sb: StringBuilder, b: B): Unit = self.encode(sb, from(b))

def encodeGreedy(sb: StringBuilder, b: B): Unit =
self.encodeGreedy(sb, from(b))
}
}

object PathEncode {

type MaybePathEncode[A] = Option[PathEncode[A]]
type Make[A] = Hinted[MaybePathEncode, A]

object Make {
def from[A](f: A => String): Make[A] = Hinted.static[MaybePathEncode, A] {
Some {
raw(f)
}
}

def raw[A](f: A => String): PathEncode[A] = {
new PathEncode[A] {
def encode(sb: StringBuilder, a: A): Unit = {
val _ = sb.append(URIEncoderDecoder.encode(f(a)))
}
def encodeGreedy(sb: StringBuilder, a: A): Unit = {
f(a).split('/').foreach {
case s if s.isEmpty() => ()
case s => sb.append('/').append(URIEncoderDecoder.encode(s))
}
}
}
}

def fromToString[A]: Make[A] = from(_.toString)

def noop[A]: Make[A] = Hinted.static[MaybePathEncode, A](None)
}

}
136 changes: 136 additions & 0 deletions modules/core/src/smithy4s/http/internals/SchematicPathEncoder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright 2021 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
package http.internals

import smithy.api.TimestampFormat
import schematic.Field
import smithy4s.internals.Hinted
import smithy.api.Http
import smithy4s.http.PathSegment
import smithy4s.http.PathSegment.StaticSegment
import smithy4s.http.PathSegment.LabelSegment
import smithy4s.http.PathSegment.GreedySegment

object SchematicPathEncoder
extends Schematic[PathEncode.Make]
with StubSchematic[PathEncode.Make] {

def default[A]: PathEncode.Make[A] = PathEncode.Make.noop
override def withHints[A](
fa: PathEncode.Make[A],
hints: smithy4s.Hints
): PathEncode.Make[A] =
fa.addHints(hints)

override def bijection[A, B](
f: PathEncode.Make[A],
to: A => B,
from: B => A
): PathEncode.Make[B] =
Hinted[PathEncode.MaybePathEncode, B](
f.hints,
make = hints => f.make(hints).map(_.contramap(from))
)

override val bigdecimal: PathEncode.Make[BigDecimal] =
PathEncode.Make.fromToString
override val bigint: PathEncode.Make[BigInt] = PathEncode.Make.fromToString
override val double: PathEncode.Make[Double] = PathEncode.Make.fromToString
override val int: PathEncode.Make[Int] = PathEncode.Make.fromToString
override val float: PathEncode.Make[Float] = PathEncode.Make.fromToString
override val short: PathEncode.Make[Short] = PathEncode.Make.fromToString
override val long: PathEncode.Make[Long] = PathEncode.Make.fromToString
override val string: PathEncode.Make[String] = PathEncode.Make.fromToString
override val uuid: PathEncode.Make[java.util.UUID] =
PathEncode.Make.fromToString
override val boolean: PathEncode.Make[Boolean] = PathEncode.Make.fromToString

override val timestamp: PathEncode.Make[Timestamp] =
Hinted[PathEncode.MaybePathEncode].from { hints =>
val fmt = hints.get(TimestampFormat).getOrElse(TimestampFormat.DATE_TIME)

Some(PathEncode.Make.raw(_.format(fmt)))
}

override val unit: PathEncode.Make[Unit] =
genericStruct(Vector.empty)(_ => ())

override def genericStruct[S](fields: Vector[Field[PathEncode.Make, S, _]])(
const: Vector[Any] => S
): PathEncode.Make[S] = {
type Writer = (StringBuilder, S) => Unit

def toPathEncoder[A](
field: Field[PathEncode.Make, S, A],
greedy: Boolean
): Option[Writer] = {
field.fold(new Field.Folder[PathEncode.Make, S, Option[Writer]] {
def onRequired[AA](
label: String,
instance: PathEncode.Make[AA],
get: S => AA
): Option[Writer] =
if (greedy)
instance.get.map(encoder =>
(sb, s) => encoder.encodeGreedy(sb, get(s))
)
else
instance.get.map(encoder => (sb, s) => encoder.encode(sb, get(s)))
def onOptional[AA](
label: String,
instance: PathEncode.Make[AA],
get: S => Option[AA]
): Option[Writer] = None
})
}

def compile1(path: PathSegment): Option[Writer] = path match {
case StaticSegment(value) =>
Some((sb, _) => { val _ = sb.append(value) })
case LabelSegment(value) =>
fields
.find(_.label == value)
.flatMap(field => toPathEncoder(field, greedy = false))
case GreedySegment(value) =>
fields
.find(_.label == value)
.flatMap(field => toPathEncoder(field, greedy = true))
}

def compilePath(path: Vector[PathSegment]): Option[Vector[Writer]] =
path.traverse(compile1(_))

Hinted[PathEncode.MaybePathEncode].onHintOpt[Http, S] { maybeHttpHint =>
for {
httpHint <- maybeHttpHint
path <- pathSegments(httpHint.uri.value)
writers <- compilePath(path)
} yield new PathEncode[S] {
def encode(sb: StringBuilder, s: S): Unit = {
var first = true
writers.foreach { w =>
if (first) { w.apply(sb, s); first = false }
else w.apply(sb.append('/'), s)
}
}

def encodeGreedy(sb: StringBuilder, s: S) = ()
}
}
}
}
Loading

0 comments on commit 56843ea

Please sign in to comment.