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

Derive http endpoints from hints #41

Merged
merged 9 commits into from
Jan 14, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
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.encodeOthers(f(a)))
}
def encodeGreedy(sb: StringBuilder, a: A): Unit = {
f(a).split('/').foreach {
case s if s.isEmpty() => ()
case s => sb.append('/').append(URIEncoderDecoder.encodeOthers(s))
}
}
}
}

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

def noop[A]: Make[A] = Hinted.static[MaybePathEncode, A](None)
kubukoz marked this conversation as resolved.
Show resolved Hide resolved
}

}
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](
kubukoz marked this conversation as resolved.
Show resolved Hide resolved
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
kubukoz marked this conversation as resolved.
Show resolved Hide resolved
})
}

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) = ()
}
}
}
}
19 changes: 19 additions & 0 deletions modules/core/src/smithy4s/http/internals/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,23 @@ package object internals {
}
}

private[smithy4s] def pathSegments(
str: String
): Option[Vector[PathSegment]] = {
str
.split('/')
.toVector
.filterNot(_.isEmpty())
.traverse(fromToString(_))
kubukoz marked this conversation as resolved.
Show resolved Hide resolved
}

private def fromToString(str: String): Option[PathSegment] = {
if (str.isEmpty()) None
else if (str.startsWith("{") && str.endsWith("+}"))
Some(PathSegment.greedy(str.substring(1, str.length() - 2)))
else if (str.startsWith("{") && str.endsWith("}"))
Some(PathSegment.label(str.substring(1, str.length() - 1)))
else Some(PathSegment.static(str))
}

}
5 changes: 0 additions & 5 deletions modules/core/src/smithy4s/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
* limitations under the License.
*/

import smithy4s.http.internals.URIEncoderDecoder

package object smithy4s extends TypeAliases {

type Hint = Hints.Binding[_]
Expand All @@ -27,9 +25,6 @@ package object smithy4s extends TypeAliases {

val errorTypeHeader = "X-Error-Type"

def segment(s: Any): String = URIEncoderDecoder.encodeOthers(s.toString())
def greedySegment(s: String) = s.split("/").map(segment).mkString("/")

// Allows to "inject" F[_] types in places that require F[_,_,_,_,_]
type GenLift[F[_]] = {
type λ[I, E, O, SI, SO] = F[O]
Expand Down
Loading